From 438bfa49ae9ff63c4692f4b55a9e854d0a124a38 Mon Sep 17 00:00:00 2001 From: Brandon Kobel Date: Fri, 12 Apr 2019 11:50:57 -0700 Subject: [PATCH] Feature Controls (#31652) (#35014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * UI/API changes to facilitate disabling features within spaces (#24235) * 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 * Begin to allow features to be disabled within spaces typescript fixes additional cleanup attempt to resolve build error fix tests more ts updates fix typedefs on manage_spaces_button more import fixes test fixes move user profile into xpack common Restructure space management screen fix SASS references design edits remove Yes/No language from feature toggles fix casing removed unused imports update snapshot fix sass reference for collapsible panel Fix sass reference, take 2 * Restructure user profile for granular app privs (#23750) merging to feature branch for further development * extract migration logic into testable unit * 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. * Design edits (#12) enables customize avatar popover update tests, and simplify editing space identifier remove references to user profile remove unused test suite remove unnecessary sass import removes security's capability_decorator * fix i18n * updates toggleUiCapabilities to use new feature definitions * cleanup and testing * remove references to old feature interface * readd lost spacer * adds feature route testing * additional i18n * snapshot update * copy edits * fix ml app icon * add missing export * remove unnecessary sass import * attempt to fix build * fix spaces api tests * esArchiver mapping updates * rename toggleUiCapabilities -> toggleUICapabilities * removes shared collapsible_panel component in favor of plugin-specific components * some copy and style adjustments * fix test following rebase * add lost types file * design edits * remove stale export * feature feedback; fixes cached disabled features * 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 * GAP - Role API Structure (#26740) * Updated the role api PUT structure * Minimum is an array now * Updating get route to naively support the new structure * Renaming and removing some serialized methods * Updating Role PUT api tests * Fixing PUT jest tests * Fixing GET tests * Updating PrivilegeSerializer tests * Renaming features to feature for the GET, so we're consistent * Validating features and feature privileges * Update x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts Co-Authored-By: kobelb * Update x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts Co-Authored-By: kobelb * Update x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts Co-Authored-By: kobelb * Renaming some variables/members of the PrivilegesSerializer * Fixing privileges serializer tests * Fixing register privileges with cluster tests * Fixing the role creation for the api integration tests * Generalizing regex within the feature registry * update tests * [GAP] - Support infra features (#26955) ## Summary This PR adds the `Infrastructure` and `Logs` apps as toggle-able features via Granular Application Privileges. * [GAP] - Enables xpack_main to populate UI Capabilities (#27031) ## Summary Currently, plugins that register features via `xpackMainPlugin.registerFeature({...})` also have to specify their own `uiCapabilities` via `injectDefaultVars`, which is counter-intuitive and cumbersome. We've accepted this complexity for OSS plugins, but x-pack and third-party plugins should not have to concern themselves with such implementation details. This PR removes that requirement for x-pack and third-party plugins, so all they have to do is register features, and ensure that their feature privileges contain the appropriate UI Capabilities in the `ui` property. ### Notes This implementation intentionally does not alter UI Capabilities that come in via OSS Kibana. The capabilities defined there should be the source of truth, regardless of which distribution is used. ### Example
xpackMainPlugin.registerFeature({
  id: 'graph',
  name: 'Graph',
  icon: 'graphApp',
  navLinkId: 'graph',
  privileges: {
    all: {
      app: [],
      savedObject: { ... },
      ui: ['showWriteControls'],
    },
    read: {
      app: [],
      savedObject: { ... },
      ui: ['someOtherCapability],
    }
  }
});
Will be translated to the following UI Capabilities: ``` uiCapabilities: { navLinks: { graph: true }, graph: { showWriteControls: true, someOtherCapability: true, } } ``` xpack_main is **not responsible** for disabling UI capabilities, so this will initialize all capabilities with a value of `true`. * Hide write controls for the visualization application (#26536) * Hide write controls for the timelion application (#26537) * blacklist feature ids (#27493) * [GAP] - Support management links (#27055) ## Summary This enables management links to be toggled via UI Capabilities. ## TODO - [x] Implement spaces controls - [x] Implement security controls - [x] Testing - [ ] (optional) - dedicated display for managing management links? * Enables the feature catalogue registry to be controlled via uiCapabil… (#27945) * Enables the feature catalogue registry to be controlled via uiCapabilities * update snapshot * xpack_main populates uiCapabilities with the full list of catalogue entries * builds application privileges using catalogue actions * prevent 'catalogue' from being registered as a feature id * fix mocha tests * fix merge * update snapshots * GAP - Discover and NavLinks Functional Testing (#27414) * Adding very basic Discover tests * Ensuring discover is visible in both spaces * Parsing the DOM to determine the uiCapabilities * Making this.wreck `any` because the type definitions suck * Specifying auth when requesting ui capabilities * Beginning architecture to support permutation testing * Adding documentation of the different configurations we'd like to test * Fixing type errors * Beginning to work on the framework to test the combinations * Adding some factories * Pushing forward, not a huge fan of what I have right now * The new-new * Less weird types * Revising some things after talking with Larry * Switching from wreck to axios * Restructuring some files * Changing to a space with all features, and a space with no features * Beginning to add the security only tests * Adding a navLinksBuilder * Adding spaces only tests * Not disabling ui capabilities, or authing app/api access when we shouldn't be * Can't get rid of management * Adding more user types * More users, this is starting to really suck * Renaming some things... * Revising which users we'll test in which ui capabilities "test suite" * Adding some more user scenarios for the security_only configuration * Adding security_only user scenarios * Adding space scenarios * Fixing type errors * Udpating the readme for the spaces we're testing with * Adding global read discover security ui functional tests * Adding tests to make sure save buttons are shown/hidden The actual implementation is broken somewhere * Fixing tests after GIS is added and conflicts happened for infra * Adding discover ui capability tests * Fixing navlinks tests * Adding discover view tests * Adding UI tests for spaces being disabled * Fixing tests * Removing wreck dependency, it's garbage * Fixing typo * Updating ui capabilities README.md and adding another user for the security and spaces ui capability tests * Updating yarn lock file * Consolidation some types * Adding VisualizeDisabledSpace to the scenarios. * Fixing esArchives with .kibana_2 * Disable features optional again * Adding ensureCurrentUrl: false * Fixing space selector tests * Fixing gis privileges, they use saved objects * Fixing find's element staleness checks * Update x-pack/test/functional/es_archives/spaces/disabled_features/data.json Co-Authored-By: kobelb * Generalizing logic to get appNavLinksText * Removing duplicate license header * Adding GIS mappingst * Fixing readme * We love our future selfs * Adding GisRead scenario * Whoops * Adding note about funky syntax for use with exhaustive switches * Using a centralized list of features * Give it some time * Even more time? * Space Management - accessibility & i18n improvements (#28195) ## Summary 1) Improves the accessibility for the spaces management screen: - Customize Space Avatar popover now receives focus when toggled - Labels are more descriptive, providing context - Delete Space modal correctly focuses 2) Improves i18n support - fixed a couple hard-coded strings 3) Adds a "Features" column to the spaces grid page, which shows a brief summary of the number of features enabled for each space: ![image](https://user-images.githubusercontent.com/3493255/50785093-93b78280-127d-11e9-975e-7209d35211ff.png) Related: https://github.com/elastic/kibana/issues/28184 * Feature Controls - spaces - functional tests (#28213) * adds tests for /api/features/v1 endpoint * update failing management test * Fc/run ui capability tests (#28362) * Running the UI Capability Tests as part of the normal CI runs * Adding uptime feature to get these tests passing * Adding features and sorting * Adding uptime security_only uiCapability tests, and fixing devTools * Fixing the docs * Fixing section panel i18n issue * Removing unused import * Updating snapshots * Feature Controls - The new new role API (#28441) These changes allow us to build the most recent UI where spaces can be "grouped" and edited at once. This changes the kibana section of the role definition to the following: ``` { kibana: [ { base: ['read'], feature: { discover: ['all'], dashboard: ['all'] }, spaces: ['*'] }, { base: ['read'], feature: { discover: ['all'], dashboard: ['all'] }, spaces: ['marketing', 'sales'] } ] } ``` If the `spaces` property isn't provided (for example if the user isn't using Spaces) then it'll default to `['*']`. There are a few other stipulations that we're implementing with this approach. 1. Each "item" can be for 1 to many spaces OR globally. We can't specify both space privileges and global privileges in the same "item" because for Spaces we translate `all` to `space_all`, etc. so we can give them different privileges, and this becomes problematic when trying to serialize/deserialize to ES. 2. Additionally, each space can only appear once. The ES model would allow this, but the role management UI becomes more complicated if we were to allow this when calculating effective privileges. * Feature Controls - Discover Save Button Test #28500 (#28501) * Adding some debug logs * Setting ui settings using the functional services * Doing the same for the spaces disabled features * Removing console.log debug statements * Using save instead of showWriteControls * Reload when adjusting visible features within the users active space (#28409) ## Summary Changes to the visible features within a space are not visible until the page is refreshed. Because of this, when a user is editing their active space, their changes are not immediately visible. This updates the space management screen to force a refresh when updating the visible features inside the active space. It also introduces a modal warning that this will happen: ![image](https://user-images.githubusercontent.com/3493255/50923423-ebd7bb80-141a-11e9-92bd-2779020578a4.png) * Throwing error if we register a feature after getAll is called (#29030) * Throwing error if we register a feature after getAll is called * Fixing some tests * Fixing feature route tests * Removing unused imports * Fixing merge conflict * Feature Controls - Fixing fallout of removing the legacy fallback (#29141) * Fixing use of mode.useRbacFoRequest to mode.useRbac * Fixing ui capability tests * [Feature Controls] - Fix a11y for customize feature section (#29174) ## Summary Fixes the displayed and announced text for the "show"/"hide" button of the Customize Visible Features section of the spaces management page. This was inadvertently broken following a merge from master at some point. * Feature Controls: Fixing k7's new "nav links" (#29198) * Fixing k7's new applist for feature controls * Renaming appSwitcher to appsMenu * Feature Controls - Dashboard (#29139) * Using addRouteSetupWork to implement the redirect * Using centralized addSetupWork * Fixing dashboard functional feature privileges tests * Ensuring landing page and create dashboard redirect to the home-page * Adding more tests to ensure the redirects work properly * Adding disabled space feature tests for Dashboards * Update src/ui/public/capabilities/route_setup.ts Co-Authored-By: kobelb * Update test/functional/page_objects/common_page.js Co-Authored-By: kobelb * Fixing ui capability tests after adding createNew * Removing unnecessary `return undefined` * requireUICapabilities -> requireUICapability * Updating dashboard ui capability tests * Fixing issue with the selection column appearing on Dashboards * Fixing ui capability dashboard space only tests * [FC] - Move management and catalogue entries out of privilege definition (#28354) * Moves catalogue and management entries from privilege defintion to base feature definition * Update new management menu to respect items disabled via UI Capabilities * add test * re-add index pattern entries * re-add advanced settings icon * fix tests * remove management and catalogue entries from read-only users * bring it back now y'all * catalogue updates for xpack plugins * Introduces 'grantWithBaseRead' flag * update privileges from all -> read where necessary * rename feature builder functions * catalogue and management items should cascade to privileges when not specified * add catalogue entry for uptime app * Simplify feature registrations using inherited catalogue/management entries * consolidate and fix privilege building logic * rename variables * remove debug code * remove duplicate lodash import * Update x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts Co-Authored-By: legrego * [GAP] - Role Management UI (#26840) ![image](https://user-images.githubusercontent.com/3493255/51063094-72310080-15c7-11e9-9274-debf0e9b84f1.png) ![image](https://user-images.githubusercontent.com/3493255/51063108-8aa11b00-15c7-11e9-92fc-31c922086e05.png) ![image](https://user-images.githubusercontent.com/3493255/51063165-cb992f80-15c7-11e9-894d-630c109211d3.png) --------- Edge-case scenarios: 1) [x] '*' and spaces in the same "entry" Handled via `_transform_errors` at the API level. Renders a partial read-only view in the UI. 2) [x] same space appearing in multiple "entries" Handled via `_transform_errors` at the API level. Renders a partial read-only view in the UI. 3) [x] base and feature privileges being set on the same "entry" UI does not allow this to be set, but UI is smart enough to display the correct effective privilege in this case. 4) [x] multiple base privileges set in the same "entry" UI does not allow this to be set, but UI is smart enough to apply the most permissive base privilege when displaying and performing privilege calculations. 5) [x] multiple feature privileges for the same "entry" (ml_all and ml_read) UI does not allow for this to be set, but UI is smart enough to apply the most permissive base privilege when displaying and performing privilege calculations. -------- ## Summary This updates the role management UI to allow application privileges to be customized globally and per-space. ## TODO: - [x] [First Design review](https://github.com/legrego/kibana/pull/13) - [ ] Second Design review - [ ] Copy review - [x] i18n - [x] Handle deleted/unknown features - [x] Handle deleted/unknown spaces - [x] Cleanup & refactoring - [x] Testing * [Feature Controls, Spaces] - Don't load bundles for hidden apps (#29617) ## Summary This enables the spaces plugin to issue a 404 if the requested application is disabled within the users active space. To enable this functionality, the `app` property was moved to the root feature level, with the option to override at the privilege level. This follows the same logic as `catalogue` and `management` sections. This will enable automatic app "protections" for those which only specify a single UI application, including: 1) Timelion 2) Canvas 3) Monitoring 4) APM 5) Code (when it merges) 6) GIS 7) Graph 8) ML * [Feature Controls] - Copy Edits (#29651) ## Summary Copy edits from today's session * [Feature Controls] - Rename-a-thon (#29709) * post-merge cleanup * [Feature Controls] - fixes from recent merge from master (#29826) ## Summary this pr will contain any required changes to fix CI from the recent merge from master, which includes the new k7 redesign design and dark mode * Feature Controls: Adding privileges tooltip for Dev Tools (#30008) * Adding privileges tooltip for Dev Tools * appeasing the linter * [Feature Controls] - Fix displayed space base privilege (#30133) ## Summary This fixes the displayed space base privilege when a global base privilege is influencing the dropdown control: 1) Add global 'read' privilege 2) Configure space privilege -- note default base privilege of 'read' 3) Change space base privilege to 'custom' Prior to this fix, the dropdown would not honor the change; it would keep 'read' as the selected option. * Feature Controls: Adding read privileges for advanced settings and index patterns (#30106) * Adding read privileges for advanced settings and index patterns * Fixing the tests and the actual code itself * Feature Controls - spaces not a security mechanism warning (#29853) * Changing copy for the spaces not a security mechanism warning * Using Gail's wording * [Feature Controls] - Fixes from merging from master (8.0) (#30267) * improve typings * fix xpack_main type definitions * test updates * Fc/functional test move (#29835) * Moving dashboard feature control tests to the dashboard application * Moving more tests around * Fixing some tests, no longer using uiSettings service, doesn't play nicely with spaces * Fixing esarchived issue * Renaming some files * [Feature Controls] - Readonly view for Advanced Settings using UICapabilities (#30243) ## Summary This builds on the work done in https://github.com/elastic/kibana/pull/30106 to enable a read-only mode for the Advanced Settings screen: - Input fields are disabled - Save options are not displayed - "Reset to default" options are not displayed * Feature Controls: No Wildcards (#30169) * A poorly named abstraction enters the room * No more wildcards, starting to move some stuff around * Splitting out the feature privilege builders * Using actions instead of relying on their implementation * We don't need the saved object types any longer * Explicitly specifying some actions that used to rely on wildcards * Fixing api integration test for privileges * Test fixture plugin which adds the globaltype now specifies a feature * Unauthorized to find unknown types now * Adding tests for features with no privileges * Update x-pack/test/saved_object_api_integration/security_and_spaces/apis/find.ts Co-Authored-By: kobelb * Adding back accidentally deleted test * Using the shared XPackMainPlugin definition * Fixing privileges * [Feature Controls] - Readonly mode for Canvas using UICapabilities (#29264) ## Summary Updates Canvas to respect UICapabilities when determining if a user has read or read/write access to the application: 1) Adds a `showWriteControls` UI Capability to the Canvas's `all` privilege. 2) Removes the `setCanUserWrite` Redux action 3) Sets the initial (and only) state for `state.transient.canUserWrite` based on the UI Capability. Closes https://github.com/elastic/kibana/issues/27695 * [Feature Controls] - Readonly mode for Maps using UICapabilities (#30437) ## Summary This updates the maps application to support a read-only mode: 1) Removes selection/delete from Maps listing page 2) Removes "save" option ## TODO: - [x] Functional UI Tests * Add typings for x-pack/test to support .html imports (#30570) We're importing `ui/capabilities` from the x-pack/test project, which implicitly traverses into typings which are potentially importing .html files, so we have to teach TypeScript about it. * [Feature Controls] - Readonly mode for Timelion using UICapabilities (#30128) ## Summary Updates Timelion to respect UICapabilities when determining if a user has read or read/write access to the application. A previous PR was responsible for hiding the save controls, but this PR adds testing and the appropriate UICapabilities to the registered privilege definition. * remove stray debug code * [Feature Controls] - Updates from src/ui move to src/legacy/ui (#30678) * dummy commit * fix import path * update message identifier * fix snapshot * remove unused translations * Feature Controls: Adding read/write privileges for all applications (#30732) * Adding read/write privileges for all applications * Using default for advanced settings, canvas, maps and timelion * Update x-pack/test/ui_capabilities/security_only/tests/canvas.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_only/tests/canvas.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_only/tests/maps.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_only/tests/maps.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_only/tests/timelion.ts Co-Authored-By: kobelb * [Feature Controls] - Readonly mode for Visualize using UICapabilities (#29714) * enable read-only view, and enable app redirection for visualize app * Hide 'Edit Visualization' dashboard context menu item if visualizations are not editable * Hide 'Add new visualization' button if action is not available * show 'Visualize' button on discover view only if viz app is available * update tests * allow visualizations to be created, but not saved for read-only users * adds functional tests for visualize * add tests for showing/hiding the visualize button in the discover app * fix visualize tests following merge from master * tests for edit viz feature from dashboards * cleanup * remove unnecessary call to set ui settings * remove unused variables * reduce flakyness of tsvb tests * renames visualize.showWriteControls => visualize.save * fix ui capability tests * fix tests * fix references to timePicker page object * fix ts errors * adds 'editable' property to embeddable metadata instead of hardcoded capability checks * Remove unnecessary read-only considerations * revert unnecessary mock changes * [Feature Controls] - Adds missing uptime icon (#30716) ## Summary Adds missing feature icon for Uptime application. Needs https://github.com/elastic/kibana/pull/30678 to merge before this will go green. * Feature Controls - Fix branch (#31135) * Updating snapshot * Switching visualize to use the default branch of the switch * Fixing esarchive * Feature Controls - Graph (#30762) * Adding graph functional tests * Fixing Privilieges API test * Adding graph ui capability tests * Update x-pack/test/ui_capabilities/security_only/tests/graph.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_only/tests/graph.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_and_spaces/tests/graph.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/security_only/tests/graph.ts Co-Authored-By: kobelb * Update x-pack/test/ui_capabilities/spaces_only/tests/graph.ts Co-Authored-By: kobelb * Apply suggestions from code review Co-Authored-By: kobelb * Removing hard-coded constants * Adding Graph delete button * Fixing ui capability tests * [Feature Controls] - Fixes page width for spaces management screen (#30723) ## Summary Fixes the skinny spaces management screen following the redesign of the overall management area. Needs https://github.com/elastic/kibana/pull/30678 to merge before this will go green. * Feature Controls - Dev Tools (#30712) * Adding functional tests * Addingn Dev_Tools ui capability tests * Adding some api tests for console's API * Apply suggestions from code review Co-Authored-By: kobelb * Moving uiCapability definition * Giving user_1 dashboard access to space_2 * Using the default in the switch for devtools/visualize * Using forceLogout, maps are leaving us on a 404 page * Fixing privileges API tests * Feature Controls- Fix Merge Conflicts (#31651) * Removing duplicated and outdated tests * Updating snapshot * Fixing type script errors * Getting rid of some double quotes * Adding saved_object:url access to discover temporarily to fix tests * Fixing dashboard tests, updating snapshots * Fixing security only find tests * Removing reduntant test * Trying to give it more time * Fixing it 20 seconds to redirect away from the create new dashboard * Feature Controls - No more route defaults for dashboards (#31767) * No more route defaults for dashboards * Verbose logging... * Changing some ciGroups to try to narrow down the problem * Revert "Verbose logging..." This reverts commit 3198e73b618e1d99c1724d21015988ca77f49672. * Revert "No more route defaults for dashboards" This reverts commit 525cd94dc52394ae94acfe5102485474c13267b0. * Chaning the method in which we do the redirect * Fixing type issue * Update index.ts * Update index.ts * Feature Controls - Only allowing features to register all and read privileges (#31526) * Only allowing features to register all and read privileges * Making all and read optional properties required some existence checks * Using Aleh's superior solution! * No more unnecessary `as any` * Feature Controls - Saved Object Management (#31332) * Adding savedObject uiCapabilities that mirror the savedobject actions * Using uiCapabilities to limit which types to search for * Restricting which saved objects can be deleted based on type * Hiding "view in app" button when we aren't allowed to * Filtering the saved objects relationships based on the valid saved object types * Using dedicated savedObjectsManagement ui capabilities * Adding readonly mode of viewing an object * Displaying View In App if you can actually do so * No more operations * Moving saved objects ui capability population to kibana plugin * Updating x-pack jest tests * Adding security only saved objects management ui capability tests * Adding security and spaces tests * Adding spaces only saved objects managment ui capability tests * Adding saved object management listing page functional tests * Adding functional tests for edit visualization * Consolidating canViewInApp and getInAppUrl into the same file * Fixing imports * One more stray import/export * Adding back esFrom source * Revert "Adding back esFrom source" This reverts commit dfb626ace3d5449d340bbe202dc782f1c08814ef. * Updating jest snapshots * Updating privileges * Adding some logging * Back to 10 seconds * Trying to get more logs... * Back to normal logging levels * Fixing ui capability tests * Putting timeouts back. * Feature Controls - UI capability API integration tests with fixture plugins (#32086) * Only testing the foo plugin for security and spaces * Using the foo plugin with the security_only tests * Changing spaces only tests to use the foo plugin * Using list of features from api, and fixing bug with the spaces interceptor * Adding catalogue tests, which are alluding to another bug * saved_objects catalogue aren't driven by ui capabilites presently * Expanding the coverage for the spaces only catalogue tests * Fixing some catalogue asserts * Fixing catalogue tests for spaces_only, I had it backwards * Adjusting Readme, adding "global read" scenario for security only tests * Responding to PR feedback * Adding back saved objects tests I accidentally deleted * Fixing typescript issues, we can't import EUI on the server * Fixing eslint error * Updating Jest snapshots, fixing chrome mock * Fixing dashboard listing test * Adding missing await and forcing logout for graph functional tests * Putting i18n string back * Fixing type script issue * Fixing canvas assert because of merge * Fixing saved object api error assertations * user-action is now a saved object type * Fixing typescript error * Fixing saved object actions as a result of the merge * Feature Controls - Infrastructure and Logging (#31843) * hide infra/logs apps if disabled via UICapabilities * adds tests * adds UICapability tests for infra and log apps * update expected privilege/action mapping * adds feature controls security tests for infraHome * adds infra spaces feature control tests * remove debug code * a sample readonly implementation, ignoring 'logs' privileges * ts fixes * fix capability expectations * Removing RequiresUICapability component, since there are no usages * Driving the source configuration seperately for logs/infrastructure * Adding infrastructure feature controls security functional tests * Adding spaces infrastructure tests * Adding logs functional tests * Reworking the ui capability tests to be more consistent * Fixing privileges API * Forcing logout * Fixing comma issue introduced by merge * Fix merge conflicts and loading/unloading esarchives more consistently * Removing unnecessary !! * Fixing saved object management tests * Fixing more tests * Using the new context APIs * Revert "Using the new context APIs" This reverts commit 4776f1fc862317fc09af15fd7f30111d0b395b1f. * Adding future version of ui capabilities react provider * Switching the order of the HOC's for infra and making the future the default * Applying Felix's PR feedback * Protecting Infra's GraphQL APIs * Updating privileges list * Using the introspection query * No longer using apollo context library, rephrasing test descriptions * Fixing issue introduced by merge conflict, I forgot a } * Putting back missplaced data test subj * Updating jest snapshots * Feature Controls - Short URLs (#32418) * Discover is showing creating short urls properly * Adding Discover functional tests * When dashboards show the share menu you can always create short urls * Visualize now displays the short urls link appropriately * Dashboard all gets access to saved objects and updating privileges api test * Updating and adding short url test to url panel content * Fixing misspelling * Updating jest snapshot * Adding comment why allowShortUrl is always true for Dashboards * Updating snapshots * Fixing snapshots, mocking chrome.getInjected * Feature Controls - Uptime (#32577) * Adding uptime functional tests * Enabling feature controls for uptime * Updating the privileges API's actions * Using a single access tag for limiting API access * Revising the behavior of maps read-only mode (#33338) * Feature Controls - APIs (#32915) * Using HapiJS's scopes to perform authorization on api endpoints * Revert "Using HapiJS's scopes to perform authorization on api endpoints" This reverts commit f73810c22d90131f765f69702da2e11183ac4637. * Switching the syntax of the api tags * Fixing privileges API * Typescriptifying some dependencies of the api authorization extensions * Using dedicated typescript file for api post auth filtering * Adding tests and restructuring the flow of the api authorization * Adjusting uptime's usage of privileges and the privileges test * Integrating PR feedback * Fixing graph test subject, thanks Joe! * Consolidating hideWriteControls dashboard listing test * Reusing maps constants * Adding type to saved object management ui capability tests * Feature Controls - Index Pattern Management (#33314) * Enabling feature controls for index patterns * Updating privileges API tests * Fixing saved object management's view index patterns in app logic * Fixing forgotten canViewInApp tests * Fixing maps spaces functional tests * Feature Controls - Differentiating the privileges with the same actions (#32266) * Differentiating the privileges with the same actions * The types for the lodash.uniqwith packare aren't right, and we need to customize the isEqual also, so we're gonna do it ourselves * Fixing dev tools ui capability * Removing are equivalent privileges prevention, it's not what we really need * Requiring all to be more permissive than read on startup * Transparently differentiating "all" from "read" feature privileges * Fixing jest tests * Adding the allHack: action to the space and global base privileges * Changing actions to be readonly * Adding JSDoc's for the Actions class and specifically the `allHack` action * Making the import of xpack_main types consistent * Feature Controls: APM (#32812) * Adding APM read privilege and adding functional UI tests * Beginning to validate the APM routes are protected properly * Protecting APM's APIs * Specifying CI group * Fixing privileges * Adding forgotten apm show ui capability * Fixing apm's privileges * Fixing merge-conflict with privileges allHack: and APM * address canvas feedback (#34269) * [Feature Controls] - Plugin postInit (#29172) ## Summary Throwing this up as a straw ~man~ person. If we like it, I can split it out and point the OSS changes against master if we'd prefer. Introduces a `postInit` plugin hook that is called after all plugins have gone through their `preInit` and `init` phases, which allows the security plugin to call `registerPrivilegesWithCluster` after all plugins have had an opportunity to register their features. * Feature Controls - Adds bulk toggle for showing/hiding features within a space (#34288) ## Summary Adds a "Change all" option to the spaces management screen to allow all features to be shown/hidden: ![image](https://user-images.githubusercontent.com/3493255/55344105-85db5d00-547a-11e9-9325-136d1c13f40e.png) Closes #34184 * Feature Controls - Unregistered Applications Authorization (#34122) * Converting the app authorization to use typescript * Adding jest tests * Only authorizing app routes that are registered for features * Using ProtectedApplications to lazily get feature applications * Removing unneeded mocked headers as part of the authorization * Adding some logging for the app authorization * Fixing imports, thanks tslint --fix! * Updating snapshots * Feature Controls - Disable privilege form until spaces are selected (#34386) ## Summary This disables the privilege selection until one or more spaces are selected in the role management form: ![image](https://user-images.githubusercontent.com/3493255/55432524-1724ff00-5561-11e9-86f6-1589ba3fa701.png) * Feature Controls - Visualize read-only create new (#34209) * Allowing users to create new visualizations, even if they can't be saved * Fixing privileges and tests * Updating snapshot * Removing visualize edit ui capability * Feature Controls - Actions Version Prefix (#34405) * Prefixing actions with version * Updating privileges api integration test * Update x-pack/plugins/security/server/lib/authorization/actions/saved_object.ts Co-Authored-By: kobelb * Requiring version to be a not empty string * Updating jest snapshots * Changing the 403 messages for the saved object client * Fixing ui/chrome mock * Feature Controls - Displaying share menu on dashboards when in read-only mode (#34207) * Displaying share menu on dashboards when in read-only mode * Fixing test description, thanks Luke! * Fixing dashboard view mode tests because the share menu is now visible * migrate from tslint to eslint * Feature Controls - Reserved Role Apps (#30525) * Removing feature privileges from ml/monitoring/apm * Adding monitoring/ml/apm as hard-coded global privileges * A poorly named abstraction enters the room * No more wildcards, starting to move some stuff around * Splitting out the feature privilege builders * Using actions instead of relying on their implementation * We don't need the saved object types any longer * Explicitly specifying some actions that used to rely on wildcards * Fixing api integration test for privileges * Test fixture plugin which adds the globaltype now specifies a feature * Unauthorized to find unknown types now * Adding reserved privileges tests * Adding reserved privileges in a designated reserved bucket * Fixing ui capability tests * Adding spaces api tests for apm/ml/monitoring users * Adding more roles to the security only ui capability tests * You can put a role with reserved privileges using the API * Adding support to get roles with _reserved privileges * Adding APM functional tests * Adding monitoring functional tests * Fixing typo * Ensuring apm_user, monitoring_user alone don't authorize you * Adding ml functional tests * Fixing test * Fixing some type errors * Updating snapshots * Fixing privileges tests * Trying to force this to run from source * Fixing TS errors * Being a less noisy neighbor * Forcing logout for apm/dashboard feature controls security tests * Fixing the security only ui capability tests * Removing test that monitoring now tests itself * Fixing some ui capability tests * Cleaning up the error page services * Fixing misspelling in comment * Using forceLogout for monitoring * Removing code that never should have been there, sorry Larry * Less leniency with the get roles * Barely alphabetical for a bit * Apply suggestions from code review Co-Authored-By: kobelb * Removing errant timeout * No more hard coded esFrom source * More nits * Adding back esFrom source * APM no longer uses reserved privileges, reserved privileges are pluggable * Fixing typescript errors * Fixing ui capability test themselves * Displaying reserved privileges for the space aware and simple forms * Removing ability to PUT roles with _reserved privileges. Removing ability to GET roles that have entries with both reserved and feature/base privileges. * Updating jest snapshots * Changing the interface for a feature to register a reserved privilege to include a description as well * Displaying features with reserved privileges in the feature table * Adjusting the reserved role privileges unit tests * Changing usages of expect.js to @kbn/expect * Changing the CalculatedPrivilege's _reserved property to reserved * Allowing reserved privileges to be assigned at kibana-* * Updating forgotten snapshot * Validating reserved privileges * Updating imports * Removing --esFrom flag, we don't need it anymore * Switching from tslint's ignore to eslint's ignore * Feature Controls - Adds feature registration to plugin generator (#34537) ## Summary This updates the plugin generator to allow plugin authors to automatically register their feature with the Feature Registry, for control via Spaces/Security. Running: ``` elastic-mbp:kibana larry$ node scripts/generate_plugin.js test-plugin ? Provide a short description An awesome Kibana plugin ? What Kibana version are you targeting? master ? Should an app component be generated? Yes ? Should translation files be generated? Yes ? Should a hack component be generated? Yes ? Should a server API be generated? Yes ? Should SCSS be used? Yes ``` Generates the following: ```js import { resolve } from 'path'; import { existsSync } from 'fs'; import { i18n } from '@kbn/i18n'; import exampleRoute from './server/routes/example'; export default function (kibana) { return new kibana.Plugin({ require: ['elasticsearch'], name: 'test_plugin', uiExports: { app: { title: 'Test Plugin', description: 'An awesome Kibana plugin', main: 'plugins/test_plugin/app', }, hacks: [ 'plugins/test_plugin/hack' ], styleSheetPaths: [resolve(__dirname, 'public/app.scss'), resolve(__dirname, 'public/app.css')].find(p => existsSync(p)), }, config(Joi) { return Joi.object({ enabled: Joi.boolean().default(true), }).default(); }, init(server, options) { // eslint-disable-line no-unused-vars const xpackMainPlugin = server.plugins.xpack_main; if (xpackMainPlugin) { const featureId = 'test_plugin'; xpackMainPlugin.registerFeature({ id: featureId, name: i18n.translate('testPlugin.featureRegistry.featureName', { defaultMessage: 'test-plugin', }), navLinkId: featureId, icon: 'discoverApp', app: [featureId, 'kibana'], catalogue: [], privileges: { all: { api: [], savedObject: { all: [], read: ['config'], }, ui: ['show'], }, read: { api: [], savedObject: { all: [], read: ['config'], }, ui: ['show'], }, }, }); } // Add server routes and initialize the plugin here exampleRoute(server); } }); } ``` * Updating core system docs * Fixing infra's dates with data for the functional tests * [Feature Controls] - Move UICapabilities to the new platform (#30585) ## Summary This moves the UI Capabilities service into the new platform, shimming into the old platform in a way that is consistent with the `i18n` service. * Fixing uptime functional api tests * Removing .only... --- ...na-plugin-public.capabilities.catalogue.md | 11 + ...a-plugin-public.capabilities.management.md | 13 + .../kibana-plugin-public.capabilities.md | 20 + ...ana-plugin-public.capabilities.navlinks.md | 11 + ...ublic.capabilitiessetup.getcapabilities.md | 11 + .../kibana-plugin-public.capabilitiessetup.md | 18 + ...na-plugin-public.coresetup.capabilities.md | 9 + .../public/kibana-plugin-public.coresetup.md | 1 + .../core/public/kibana-plugin-public.md | 2 + docs/development/security/rbac.asciidoc | 12 +- .../sao_template/sao.test.js | 8 +- .../sao_template/template/index.js | 45 +- .../table/listing_table/listing_table.js | 18 +- .../table/listing_table/listing_table_row.js | 17 +- .../capabilities/capabilities_service.mock.ts | 45 + .../capabilities/capabilities_service.test.ts | 60 + .../capabilities/capabilities_service.tsx | 72 + src/core/public/capabilities/index.ts | 20 + src/core/public/core_system.ts | 6 + src/core/public/index.ts | 4 + .../injected_metadata_service.ts | 2 +- src/core/public/kibana.api.md | 17 + .../__snapshots__/legacy_service.test.ts.snap | 2 + src/core/public/legacy/legacy_service.test.ts | 22 + src/core/public/legacy/legacy_service.ts | 2 + .../deep_freeze.test.ts | 0 .../deep_freeze.ts | 0 .../frozen_object_mutation/index.ts | 0 .../frozen_object_mutation/tsconfig.json | 0 .../integration_tests/deep_freeze.test.ts | 0 src/es_archiver/lib/indices/kibana_index.js | 1 + src/legacy/core_plugins/console/index.js | 7 +- .../core_plugins/console/public/console.js | 2 + .../console/server/proxy_route.js | 1 + src/legacy/core_plugins/kibana/index.js | 54 +- .../kibana/public/dashboard/dashboard_app.js | 1 + .../public/dashboard/dashboard_config.js | 4 +- .../kibana/public/dashboard/index.js | 5 +- .../dashboard_listing.test.js.snap | 12 +- .../dashboard/listing/dashboard_listing.js | 7 +- .../panel_actions/get_edit_panel_action.tsx | 8 +- .../public/dashboard/top_nav/add_panel.js | 27 +- .../dashboard/top_nav/add_panel.test.js | 10 + .../dashboard/top_nav/get_top_nav_config.js | 3 +- .../field_chooser/discover_field.js | 3 + .../lib/detail_views/string.html | 2 +- .../public/discover/controllers/discover.js | 226 ++- .../discover/embeddable/search_embeddable.ts | 3 + .../embeddable/search_embeddable_factory.ts | 4 +- .../kibana/public/home/home_ng_wrapper.html | 1 + .../core_plugins/kibana/public/kibana.js | 1 + .../kibana/public/management/app.html | 4 +- .../kibana/public/management/index.js | 6 +- .../create_button/create_button.tsx | 13 +- .../sections/index_patterns/index.js | 16 +- .../index_pattern_table.tsx | 2 +- .../management/sections/objects/_objects.js | 7 +- .../management/sections/objects/_view.html | 25 +- .../management/sections/objects/_view.js | 8 +- .../__snapshots__/objects_table.test.js.snap | 3 + .../__jest__/objects_table.test.js | 101 +- .../objects_table/components/flyout/flyout.js | 4 +- .../__snapshots__/relationships.test.js.snap | 12 + .../__jest__/relationships.test.js | 6 + .../components/relationships/relationships.js | 5 +- .../__jest__/__snapshots__/table.test.js.snap | 205 ++ .../components/table/__jest__/table.test.js | 16 +- .../objects_table/components/table/table.js | 17 +- .../components/objects_table/objects_table.js | 26 +- .../lib/__jest__/get_in_app_url.test.js | 53 - .../lib/__jest__/get_relationships.test.js | 6 +- .../objects/lib/__jest__/in_app_url.test.js | 150 ++ .../sections/objects/lib/get_relationships.js | 7 +- .../lib/{get_in_app_url.js => in_app_url.js} | 20 + .../management/sections/objects/lib/index.js | 2 +- .../advanced_settings.test.js.snap | 180 ++ .../sections/settings/advanced_settings.js | 2 + .../settings/advanced_settings.test.js | 14 + .../field/__snapshots__/field.test.js.snap | 666 +++++- .../settings/components/field/field.js | 17 +- .../settings/components/field/field.test.js | 22 + .../form/__snapshots__/form.test.js.snap | 194 ++ .../sections/settings/components/form/form.js | 2 + .../settings/components/form/form.test.js | 20 + .../management/sections/settings/index.js | 5 +- .../public/table_list_view/table_list_view.js | 17 +- .../kibana/public/visualize/editor/editor.js | 7 +- .../embeddable/visualize_embeddable.ts | 3 + .../visualize_embeddable_factory.ts | 3 + .../kibana/public/visualize/index.js | 4 +- .../listing/visualize_listing_table.js | 9 +- .../management/saved_objects/relationships.js | 7 +- .../management/saved_objects/relationships.js | 5 +- .../tests_bundle/tests_entry_template.js | 18 + src/legacy/core_plugins/timelion/index.js | 10 +- .../core_plugins/timelion/public/app.js | 235 ++- .../timelion/public/partials/save_sheet.html | 3 +- .../plugin_spec/plugin_spec.js | 6 + src/legacy/server/plugins/initialize_mixin.js | 1 + src/legacy/server/plugins/lib/plugin.js | 8 + src/legacy/ui/public/capabilities/index.ts | 31 + .../ui/public/capabilities/react/index.ts | 21 + .../react/inject_ui_capabilities.test.tsx | 113 ++ .../react/inject_ui_capabilities.tsx | 53 + .../public/capabilities/react/legacy/index.ts | 21 + .../legacy/inject_ui_capabilities.test.tsx | 113 ++ .../react/legacy/inject_ui_capabilities.tsx | 57 + .../react/legacy/ui_capabilities_provider.tsx | 48 + .../react/ui_capabilities_context.ts | 27 + .../react/ui_capabilities_provider.tsx | 38 + .../ui/public/capabilities/route_setup.ts | 38 + .../header_global_nav/components/header.tsx | 14 +- .../header_global_nav/header_global_nav.js | 5 +- src/legacy/ui/public/embeddable/embeddable.ts | 5 + .../management/components/sidebar_nav.test.ts | 10 +- .../management/components/sidebar_nav.tsx | 6 +- src/legacy/ui/public/management/section.js | 9 +- .../{__tests__/section.js => section.test.js} | 90 +- .../ui/public/registry/feature_catalogue.js | 6 +- .../public/registry/feature_catalogue.test.js | 97 + .../ui/public/routes/route_manager.d.ts | 2 + src/legacy/ui/public/routes/routes.d.ts | 1 + .../url_panel_content.test.js.snap | 110 + .../share/components/share_context_menu.tsx | 3 + .../components/url_panel_content.test.js | 12 + .../share/components/url_panel_content.tsx | 8 +- .../public/share/show_share_context_menu.tsx | 3 + src/legacy/ui/ui_render/ui_render_mixin.js | 15 +- .../management/saved_objects/relationships.js | 86 +- test/functional/config.js | 3 + test/functional/page_objects/common_page.js | 65 +- .../functional/page_objects/dashboard_page.js | 7 +- test/functional/page_objects/discover_page.js | 8 + test/functional/page_objects/error_page.js | 48 + test/functional/page_objects/index.ts | 3 + test/functional/page_objects/settings_page.js | 49 + test/functional/page_objects/share_page.js | 8 + test/functional/page_objects/timelion_page.js | 16 + .../functional/page_objects/visualize_page.js | 9 +- x-pack/dev-tools/jest/create_jest_config.js | 1 + x-pack/package.json | 1 + x-pack/plugins/__mocks__/ui/capabilities.ts | 29 + x-pack/plugins/__mocks__/ui/chrome.js | 4 + x-pack/plugins/apm/index.ts | 31 + .../apm/public/components/app/Main/index.tsx | 2 +- x-pack/plugins/apm/server/routes/errors.ts | 12 +- x-pack/plugins/apm/server/routes/metrics.ts | 3 +- x-pack/plugins/apm/server/routes/services.ts | 6 +- x-pack/plugins/apm/server/routes/traces.ts | 6 +- .../apm/server/routes/transaction_groups.ts | 15 +- x-pack/plugins/canvas/init.js | 25 + .../canvas/public/apps/workpad/routes.js | 20 - .../workpad_header/workpad_header.js | 1 + .../public/components/workpad_loader/index.js | 25 +- .../workpad_loader/workpad_create.js | 9 +- .../canvas/public/state/actions/transient.js | 1 - .../canvas/public/state/initial_state.js | 4 +- .../canvas/public/state/reducers/transient.js | 4 - .../canvas/public/state/selectors/app.js | 4 - .../auto_follow_pattern_add.test.js | 9 + .../auto_follow_pattern_edit.test.js | 9 + .../follower_index_add.test.js | 9 + .../follower_index_edit.test.js | 9 + .../__jest__/client_integration/home.test.js | 10 + x-pack/plugins/graph/index.js | 29 +- x-pack/plugins/graph/public/app.js | 188 +- .../grokdebugger/grokdebugger_route.js | 3 + .../plugins/infra/public/apps/start_app.tsx | 29 +- .../fields_configuration_panel.tsx | 35 +- .../indices_configuration_panel.tsx | 4 + .../name_configuration_panel.tsx | 3 + .../source_configuration_flyout.tsx | 74 +- .../infra/public/components/waffle/node.tsx | 1 + .../components/waffle/node_context_menu.tsx | 177 +- x-pack/plugins/infra/public/pages/404.tsx | 2 +- .../pages/infrastructure/snapshot/index.tsx | 208 +- .../plugins/infra/public/pages/logs/page.tsx | 2 +- .../infra/public/pages/logs/page_header.tsx | 74 +- .../pages/logs/page_no_indices_content.tsx | 107 +- .../infra/public/pages/metrics/index.tsx | 376 ++-- x-pack/plugins/infra/public/routes.tsx | 29 +- x-pack/plugins/infra/server/kibana.index.ts | 60 + .../framework/kibana_framework_adapter.ts | 6 + x-pack/plugins/maps/index.js | 27 + .../public/angular/listing_ng_wrapper.html | 1 + .../maps/public/angular/map_controller.js | 9 +- x-pack/plugins/maps/public/index.js | 2 + .../public/shared/components/map_listing.js | 22 +- x-pack/plugins/ml/index.js | 24 + .../jobs_list_view/jobs_list_view.js | 2 +- x-pack/plugins/monitoring/init.js | 25 + .../remote_clusters_list.test.js | 10 + .../job_create_date_histogram.test.js | 1 + .../job_create_histogram.test.js | 1 + .../job_create_logistics.test.js | 1 + .../job_create_metrics.test.js | 1 + .../job_create_review.test.js | 1 + .../job_create_terms.test.js | 1 + .../client_integration/job_list.test.js | 10 + .../sections/job_list/job_list.test.js | 9 + x-pack/plugins/searchprofiler/public/app.js | 2 + .../public/templates/index.html | 2 +- .../common/{constants.js => constants.ts} | 2 + ...ex_privilege.ts => features_privileges.ts} | 9 +- x-pack/plugins/security/common/model/index.ts | 10 + .../kibana_privileges/feature_privileges.ts | 36 + .../kibana_privileges/global_privileges.ts | 17 + .../common/model/kibana_privileges/index.ts | 7 + .../kibana_privileges/kibana_privileges.ts | 26 + .../kibana_privileges/spaces_privileges.ts | 17 + .../common/model/raw_kibana_privileges.ts | 18 + x-pack/plugins/security/common/model/role.ts | 30 +- .../common/privilege_calculator_utils.test.ts | 88 + .../common/privilege_calculator_utils.ts | 58 + x-pack/plugins/security/index.js | 68 +- .../security/public/components/_index.scss | 1 + .../components/management/users/_index.scss | 1 + .../components/management/users/_users.scss | 16 + .../components/management/users/edit_user.js | 4 +- .../components/management/users/users.js | 6 +- x-pack/plugins/security/public/index.scss | 19 +- .../__fixtures__/build_role.ts | 38 + .../__fixtures__/common_allowed_privileges.ts | 52 + .../default_privilege_definition.ts | 38 + .../__fixtures__/index.ts | 9 + .../lib/kibana_privilege_calculator/index.ts | 8 + ...bana_allowed_privileges_calculator.test.ts | 296 +++ .../kibana_allowed_privileges_calculator.ts | 153 ++ .../kibana_base_privilege_calculator.test.ts | 321 +++ .../kibana_base_privilege_calculator.ts | 99 + ...ibana_feature_privilege_calculator.test.ts | 868 ++++++++ .../kibana_feature_privilege_calculator.ts | 198 ++ .../kibana_privilege_calculator.test.ts | 791 ++++++++ .../kibana_privilege_calculator.ts | 114 ++ .../kibana_privilege_calculator_types.ts | 61 + .../kibana_privileges_calculator_factory.ts | 77 + .../public/lib/privilege_utils.test.ts | 85 + .../security/public/lib/privilege_utils.ts | 27 + .../lib/{role.test.ts => role_utils.test.ts} | 63 +- .../public/lib/{role.ts => role_utils.ts} | 22 +- .../lib/transform_role_for_save.test.ts | 484 +++++ .../public/lib/transform_role_for_save.ts | 41 + .../security/public/objects/lib/roles.ts | 10 +- .../plugins/security/public/views/_index.scss | 8 + .../public/views/management/_index.scss | 2 + .../_change_password_form.scss | 17 + .../change_password_form/_index.scss | 1 + .../change_password_form.html | 2 +- .../views/management/edit_role/_index.scss | 1 + .../edit_role/components/_index.scss | 9 + .../collapsible_panel.test.tsx.snap | 0 .../_collapsible_panel.scss} | 0 .../collapsible_panel.test.tsx | 0 .../collapsible_panel.tsx | 29 +- .../components/collapsible_panel/index.ts | 7 + .../components/delete_role_button.tsx | 12 +- .../components/edit_role_page.test.tsx | 562 +++++ .../edit_role/components/edit_role_page.tsx | 52 +- .../cluster_privileges.test.tsx.snap | 21 - .../elasticsearch_privileges.test.tsx.snap | 35 +- .../index_privilege_form.test.tsx.snap | 25 +- .../privileges/es/cluster_privileges.test.tsx | 7 +- .../privileges/es/cluster_privileges.tsx | 7 +- .../es/elasticsearch_privileges.test.tsx | 15 +- .../es/elasticsearch_privileges.tsx | 22 +- .../es/index_privilege_form.test.tsx | 8 +- .../privileges/es/index_privilege_form.tsx | 43 +- .../privileges/es/index_privileges.test.tsx | 10 +- .../privileges/es/index_privileges.tsx | 17 +- .../edit_role/components/privileges/index.ts | 2 +- .../impacted_spaces_flyout.test.tsx.snap | 21 - .../kibana_privileges.test.tsx.snap | 56 - .../kibana_privileges_region.test.tsx.snap | 78 + .../privilege_callout_warning.test.tsx.snap | 248 --- .../privilege_space_form.test.tsx.snap | 91 - .../simple_privilege_form.test.tsx.snap | 50 - .../space_aware_privilege_form.test.tsx.snap | 427 ---- .../space_selector.test.tsx.snap | 14 - .../__snapshots__/feature_table.test.tsx.snap | 41 + .../kibana/feature_table/_index.scss | 3 + .../feature_table/change_all_privileges.tsx | 79 + .../feature_table/feature_table.test.tsx | 150 ++ .../kibana/feature_table/feature_table.tsx | 276 +++ .../privileges/kibana/feature_table/index.ts | 7 + .../kibana/impacted_spaces_flyout.scss | 8 - .../kibana/impacted_spaces_flyout.test.tsx | 170 -- .../kibana/impacted_spaces_flyout.tsx | 149 -- .../kibana/kibana_privileges.test.tsx | 68 - .../privileges/kibana/kibana_privileges.tsx | 72 - .../kibana/kibana_privileges_region.test.tsx | 92 + .../kibana/kibana_privileges_region.tsx | 89 + .../kibana/privilege_callout_warning.test.tsx | 17 - .../kibana/privilege_callout_warning.tsx | 179 -- .../kibana/privilege_space_form.test.tsx | 48 - .../kibana/privilege_space_form.tsx | 113 -- .../kibana/privilege_space_table.tsx | 209 -- .../kibana/simple_privilege_form.test.tsx | 82 - .../kibana/simple_privilege_form.tsx | 88 - .../simple_privilege_section.test.tsx.snap | 178 ++ .../kibana/simple_privilege_section/index.ts | 7 + .../privilege_selector.tsx | 11 +- .../simple_privilege_section.test.tsx | 239 +++ .../simple_privilege_section.tsx | 343 ++++ .../unsupported_space_privileges_warning.tsx | 24 + .../space_aware_privilege_form.test.tsx | 253 --- .../kibana/space_aware_privilege_form.tsx | 456 ----- .../privilege_display.test.tsx.snap | 118 ++ .../privilege_space_form.test.tsx.snap | 516 +++++ ...pace_aware_privilege_section.test.tsx.snap | 44 + .../space_aware_privilege_section/_index.scss | 1 + .../_privilege_matrix.scss | 14 + .../space_aware_privilege_section/index.ts | 7 + .../privilege_display.test.tsx | 53 + .../privilege_display.tsx | 176 ++ .../privilege_matrix.test.tsx | 128 ++ .../privilege_matrix.tsx | 348 ++++ .../privilege_space_form.test.tsx | 76 + .../privilege_space_form.tsx | 582 ++++++ .../privilege_space_table.tsx | 312 +++ .../space_aware_privilege_section.test.tsx | 209 ++ .../space_aware_privilege_section.tsx | 275 +++ .../space_selector.tsx | 99 + .../privileges/kibana/space_selector.test.tsx | 17 - .../privileges/kibana/space_selector.tsx | 68 - .../kibana/transform_error_section/index.ts | 7 + .../transform_error_section.tsx | 36 + .../components/reserved_role_badge.test.tsx | 44 +- .../components/reserved_role_badge.tsx | 7 +- .../_spaces_popover_list.scss | 3 + .../components/spaces_popover_list/index.ts | 7 + .../spaces_popover_list.tsx | 206 ++ .../views/management/edit_role/index.js | 29 +- .../get_available_privileges.test.ts.snap | 3 - .../management/edit_role/lib/constants.ts | 17 +- .../edit_role/lib/copy_role.test.ts | 34 - .../lib/get_available_privileges.test.ts | 27 - .../edit_role/lib/get_available_privileges.ts | 21 - .../edit_role/lib/validate_role.test.ts | 237 +-- .../management/edit_role/lib/validate_role.ts | 82 +- .../public/views/management/index.scss | 39 - .../public/views/management/roles.html | 2 +- .../security/public/views/management/roles.js | 6 +- .../optional_plugin.test.ts.snap | 13 + .../security/server/lib/audit_logger.test.js | 2 +- .../__snapshots__/actions.test.js.snap | 25 - .../check_privileges.test.js.snap | 27 - .../check_privileges.test.ts.snap | 27 + .../disable_ui_capabilities.test.ts.snap | 3 + .../privilege_serializer.test.ts.snap | 23 + .../privileges_serializer.test.ts.snap | 5 + .../resource_serializer.test.ts.snap | 3 + ...ication_privileges_serializer.test.js.snap | 5 - ...snap => validate_es_response.test.ts.snap} | 0 .../server/lib/authorization/actions.js | 27 - .../server/lib/authorization/actions.test.js | 79 - .../actions/__snapshots__/api.test.ts.snap | 13 + .../actions/__snapshots__/app.test.ts.snap | 13 + .../__snapshots__/saved_object.test.ts.snap | 25 + .../actions/__snapshots__/ui.test.ts.snap | 25 + .../lib/authorization/actions/actions.test.ts | 57 + .../lib/authorization/actions/actions.ts | 53 + .../lib/authorization/actions/api.test.ts | 30 + .../server/lib/authorization/actions/api.ts | 26 + .../lib/authorization/actions/app.test.ts | 30 + .../server/lib/authorization/actions/app.ts | 26 + .../server/lib/authorization/actions/index.ts | 7 + .../actions/saved_object.test.ts | 39 + .../lib/authorization/actions/saved_object.ts | 31 + .../lib/authorization/actions/space.test.ts | 16 + .../server/lib/authorization/actions/space.ts | 17 + .../lib/authorization/actions/ui.test.ts | 65 + .../server/lib/authorization/actions/ui.ts | 55 + .../authorization/api_authorization.test.ts | 193 ++ .../lib/authorization/api_authorization.ts | 40 + .../authorization/app_authorization.test.ts | 198 ++ .../lib/authorization/app_authorization.ts | 72 + .../lib/authorization/check_privileges.js | 88 - .../authorization/check_privileges.test.js | 867 -------- .../authorization/check_privileges.test.ts | 1000 +++++++++ .../lib/authorization/check_privileges.ts | 168 ++ .../check_privileges_dynamically.test.ts | 55 + .../check_privileges_dynamically.ts | 41 + .../disable_ui_capabilities.test.ts | 478 +++++ .../authorization/disable_ui_capabilities.ts | 153 ++ .../server/lib/authorization/index.js | 10 - .../server/lib/authorization/index.ts | 17 + .../security/server/lib/authorization/mode.js | 21 - .../{mode.test.js => mode.test.ts} | 25 +- .../security/server/lib/authorization/mode.ts | 34 + .../privilege_serializer.test.ts | 219 ++ .../lib/authorization/privilege_serializer.ts | 100 + .../server/lib/authorization/privileges.js | 69 - .../lib/authorization/privileges.test.js | 76 - .../feature_privilege_builder/api.ts | 21 + .../feature_privilege_builder/app.ts | 23 + .../feature_privilege_builder/catalogue.ts | 25 + .../feature_privilege_builder.ts | 20 + .../feature_privilege_builder/index.ts | 38 + .../feature_privilege_builder/management.ts | 28 + .../feature_privilege_builder/navlink.ts | 17 + .../feature_privilege_builder/saved_object.ts | 33 + .../saved_objects_management.ts | 29 + .../feature_privilege_builder/ui.ts | 17 + .../lib/authorization/privileges/index.ts | 7 + .../privileges/privileges.test.ts | 941 +++++++++ .../authorization/privileges/privileges.ts | 88 + .../privileges_serializer.test.ts | 226 +++ .../authorization/privileges_serializer.ts | 91 + .../register_privileges_with_cluster.js | 41 +- .../register_privileges_with_cluster.test.js | 787 ++++++- .../authorization/resource_serializer.test.ts | 25 + .../lib/authorization/resource_serializer.ts | 25 + .../server/lib/authorization/service.js | 27 - .../server/lib/authorization/service.test.js | 69 - .../lib/authorization/service.test.mocks.ts | 35 + .../server/lib/authorization/service.test.ts | 98 + .../server/lib/authorization/service.ts | 64 + ...space_application_privileges_serializer.js | 35 - ..._application_privileges_serializer.test.js | 51 - .../server/lib/authorization/types.ts | 19 + ...e.test.js => validate_es_response.test.ts} | 129 +- ...es_response.js => validate_es_response.ts} | 29 +- .../validate_feature_privileges.test.ts | 100 + .../validate_feature_privileges.ts | 25 + .../server/lib/capability_decorator.ts | 61 + .../server/lib/optional_plugin.test.ts | 85 + .../security/server/lib/optional_plugin.ts | 48 + .../secure_saved_objects_client_wrapper.js | 23 +- ...ecure_saved_objects_client_wrapper.test.js | 1622 +++------------ .../routes/api/public/privileges/get.test.ts | 142 ++ .../routes/api/public/privileges/get.ts | 48 + .../routes/api/public/privileges/index.ts | 14 + .../server/routes/api/public/roles/get.js | 154 +- .../routes/api/public/roles/get.test.js | 1803 ++++++++++++++--- .../server/routes/api/public/roles/index.js | 8 +- .../server/routes/api/public/roles/put.js | 88 +- .../routes/api/public/roles/put.test.js | 465 ++++- .../spaces/common/is_reserved_space.test.ts | 2 + x-pack/plugins/spaces/common/model/space.ts | 1 + x-pack/plugins/spaces/index.ts | 54 +- x-pack/plugins/spaces/mappings.json | 3 + .../components/manage_spaces_button.test.tsx | 28 +- .../components/manage_spaces_button.tsx | 5 +- .../views/components/space_card.test.tsx | 2 + .../views/components/space_cards.test.tsx | 1 + .../public/views/management/_index.scss | 2 +- .../views/management/_manage_spaces.scss | 9 - .../confirm_delete_modal.test.tsx.snap | 4 +- .../advanced_settings_subtitle.test.tsx | 1 + .../advanced_settings_title.test.tsx.snap | 1 + .../advanced_settings_title.test.tsx | 1 + .../components/confirm_delete_modal.test.tsx | 2 + .../components/confirm_delete_modal.tsx | 13 +- .../secure_space_message.test.tsx.snap | 6 +- .../secure_space_message.test.tsx | 37 +- .../secure_space_message.tsx | 16 +- .../customize_space_avatar.test.tsx.snap | 27 - ...irm_alter_active_space_modal.test.tsx.snap | 29 + .../confirm_alter_active_space_modal.test.tsx | 22 + .../confirm_alter_active_space_modal.tsx | 52 + .../confirm_alter_active_space_modal/index.ts | 7 + .../customize_space_avatar.test.tsx.snap | 47 + .../space_identifier.test.tsx.snap | 30 +- .../customize_space/customize_space.tsx | 247 +++ .../customize_space_avatar.test.tsx | 21 +- .../customize_space_avatar.tsx | 43 +- .../edit_space/customize_space/index.ts | 7 + .../space_identifier.test.tsx | 4 +- .../space_identifier.tsx | 95 +- .../edit_space/delete_spaces_button.test.tsx | 1 + .../enabled_features.test.tsx.snap | 327 +++ .../edit_space/enabled_features/_index.scss | 3 + .../enabled_features.test.tsx | 129 ++ .../enabled_features/enabled_features.tsx | 155 ++ .../enabled_features/feature_table.tsx | 113 ++ .../edit_space/enabled_features/index.ts | 7 + .../enabled_features/toggle_all_features.tsx | 99 + .../edit_space/manage_space_page.test.tsx | 194 +- .../edit_space/manage_space_page.tsx | 278 +-- .../edit_space/reserved_space_badge.test.tsx | 2 + .../__snapshots__/section_panel.test.tsx.snap | 51 + .../section_panel/_section_panel.scss | 4 + .../edit_space/section_panel/index.ts | 7 + .../section_panel/section_panel.test.tsx | 66 + .../section_panel/section_panel.tsx | 137 ++ .../management/lib/feature_utils.test.ts | 55 + .../views/management/lib/feature_utils.ts | 13 + .../management/lib/validate_space.test.ts | 42 +- .../views/management/lib/validate_space.ts | 11 +- .../public/views/management/page_routes.tsx | 37 +- .../spaces_grid_pages.test.tsx.snap | 28 +- .../spaces_grid/spaces_grid_page.tsx | 72 +- .../spaces_grid/spaces_grid_pages.test.tsx | 12 +- .../nav_control_popover.test.tsx.snap | 6 +- .../spaces_description.test.tsx.snap | 5 - .../components/spaces_description.test.tsx | 9 +- .../components/spaces_description.tsx | 3 - .../nav_control/components/spaces_menu.tsx | 3 - .../public/views/nav_control/nav_control.tsx | 5 - .../nav_control/nav_control_popover.test.tsx | 8 +- .../views/nav_control/nav_control_popover.tsx | 10 +- .../space_selector/space_selector.test.tsx | 2 + .../server/lib/create_default_space.test.ts | 1 + .../spaces/server/lib/create_default_space.ts | 1 + .../spaces/server/lib/migrations/index.ts | 7 + .../server/lib/migrations/migrate_6x.test.ts | 40 + .../server/lib/migrations/migrate_6x.ts | 12 + .../server/lib/request_inteceptors/index.ts | 13 + .../on_post_auth_interceptor.test.ts | 404 ++++ .../on_post_auth_interceptor.ts} | 87 +- .../on_request_interceptor.test.ts | 194 ++ .../on_request_interceptor.ts | 38 + .../lib/space_request_interceptors.test.ts | 440 ---- .../plugins/spaces/server/lib/space_schema.ts | 1 + .../spaces/server/lib/spaces_client.test.ts | 32 +- .../spaces/server/lib/spaces_client.ts | 8 +- .../server/lib/toggle_ui_capabilities.test.ts | 169 ++ .../server/lib/toggle_ui_capabilities.ts | 71 + .../lib/copy_role.ts => spaces/types.d.ts} | 8 +- .../translations/translations/zh-CN.json | 117 +- x-pack/plugins/uptime/index.ts | 5 +- x-pack/plugins/uptime/public/uptime_app.tsx | 2 +- x-pack/plugins/uptime/server/index.ts | 2 +- x-pack/plugins/uptime/server/kibana.index.ts | 36 +- .../framework/kibana_framework_adapter.ts | 3 + .../uptime/server/rest_api/auth/is_valid.ts | 3 + .../uptime/server/rest_api/pings/get_all.ts | 1 + x-pack/plugins/xpack_main/index.js | 5 +- .../public/services/user_profile.test.ts | 42 - .../public/services/user_profile.ts | 31 - .../lib/__tests__/replace_injected_vars.js | 105 +- .../feature_registry.test.ts.snap | 9 + .../feature_registry/feature_registry.test.ts | 338 +++ .../lib/feature_registry/feature_registry.ts | 196 ++ .../server/lib/feature_registry/index.ts | 13 + .../lib/populate_ui_capabilities.test.ts | 302 +++ .../server/lib/populate_ui_capabilities.ts | 84 + .../server/lib/register_oss_features.ts | 224 ++ .../server/lib/replace_injected_vars.js | 17 +- .../xpack_main/server/lib/setup_xpack_main.js | 5 + .../server/lib/user_profile_registry.test.ts | 36 - .../server/lib/user_profile_registry.ts | 32 - .../server/lib/xpack_info_license.d.ts | 2 +- .../__snapshots__/features.test.ts.snap | 34 + .../routes/api/v1/features/features.test.ts | 80 + .../server/routes/api/v1/features/features.ts | 26 + .../server/routes/api/v1/features/index.ts | 7 + .../xpack_main/server/routes/api/v1/index.js | 1 + x-pack/plugins/xpack_main/types.ts | 11 + x-pack/plugins/xpack_main/xpack_main.d.ts | 4 + x-pack/scripts/functional_tests.js | 3 + .../apis/apm/feature_controls.ts | 338 +++ x-pack/test/api_integration/apis/apm/index.ts | 16 + .../apis/console/feature_controls.ts | 218 ++ .../api_integration/apis/console/index.ts | 16 + x-pack/test/api_integration/apis/index.js | 2 + .../apis/infra/feature_controls.ts | 297 +++ .../test/api_integration/apis/infra/index.js | 1 + .../test/api_integration/apis/infra/types.ts | 9 + .../apis/kibana/stats/stats.js | 8 - .../api_integration/apis/security/index.js | 1 + .../apis/security/privileges.ts | 1341 ++++++++++++ .../api_integration/apis/security/roles.js | 153 +- .../api_integration/apis/uptime/constants.ts | 10 + .../apis/uptime/feature_controls.ts | 276 +++ .../apis/uptime/get_all_pings.js | 13 +- .../test/api_integration/apis/uptime/index.js | 1 + .../apis/xpack_main/features/features.ts | 49 + .../apis/xpack_main/features/index.ts | 14 + .../api_integration/apis/xpack_main/index.js | 1 + x-pack/test/api_integration/config.js | 13 +- x-pack/test/api_integration/services/index.js | 2 +- .../services/infraops_graphql_client.js | 35 +- x-pack/test/common/services/index.ts | 8 + x-pack/test/common/services/security/index.ts | 28 + x-pack/test/common/services/security/role.ts | 47 + x-pack/test/common/services/security/user.ts | 53 + x-pack/test/common/services/spaces/index.ts | 56 + .../advanced_settings_security.ts | 190 ++ .../advanced_settings_spaces.ts | 90 + .../apps/advanced_settings/index.ts | 18 + .../apps/apm/feature_controls/apm_security.ts | 173 ++ .../apps/apm/feature_controls/apm_spaces.ts | 80 + .../apps/apm/feature_controls/index.ts | 14 + x-pack/test/functional/apps/apm/index.ts | 14 + .../feature_controls/canvas_security.ts | 250 +++ .../canvas/feature_controls/canvas_spaces.ts | 150 ++ x-pack/test/functional/apps/canvas/index.js | 2 + .../feature_controls/dashboard_security.ts | 342 ++++ .../feature_controls/dashboard_spaces.ts | 155 ++ .../apps/dashboard/feature_controls/index.ts | 14 + .../test/functional/apps/dashboard/index.ts | 15 + .../dashboard_mode/dashboard_view_mode.js | 10 +- .../feature_controls/dev_tools_security.ts | 205 ++ .../feature_controls/dev_tools_spaces.ts | 112 + .../apps/dev_tools/feature_controls/index.ts | 14 + .../test/functional/apps/dev_tools/index.ts | 15 + .../feature_controls/discover_security.ts | 253 +++ .../feature_controls/discover_spaces.ts | 147 ++ .../apps/discover/feature_controls/index.ts | 14 + x-pack/test/functional/apps/discover/index.ts | 15 + .../graph/feature_controls/graph_security.ts | 194 ++ .../graph/feature_controls/graph_spaces.ts | 93 + .../apps/graph/feature_controls/index.ts | 14 + x-pack/test/functional/apps/graph/index.js | 1 + .../index_patterns/feature_controls/index.ts | 14 + .../index_patterns_security.ts | 195 ++ .../feature_controls/index_patterns_spaces.ts | 85 + .../functional/apps/index_patterns/index.ts | 17 + .../apps/infra/feature_controls/index.ts | 16 + .../infrastructure_security.ts | 448 ++++ .../feature_controls/infrastructure_spaces.ts | 240 +++ .../infra/feature_controls/logs_security.ts | 197 ++ .../infra/feature_controls/logs_spaces.ts | 98 + x-pack/test/functional/apps/infra/index.ts | 1 + .../feature_controls/index.ts | 14 + .../feature_controls/ml_security.ts | 115 ++ .../feature_controls/ml_spaces.ts | 93 + .../functional/apps/machine_learning/index.ts | 15 + .../maps/feature_controls/maps_security.ts | 212 ++ .../apps/maps/feature_controls/maps_spaces.ts | 97 + x-pack/test/functional/apps/maps/index.js | 2 + .../apps/monitoring/feature_controls/index.ts | 14 + .../feature_controls/monitoring_security.ts | 111 + .../feature_controls/monitoring_spaces.ts | 90 + .../test/functional/apps/monitoring/index.js | 2 + .../saved_objects_management_security.ts | 232 +++ .../apps/saved_objects_management/index.ts | 17 + .../functional/apps/security/rbac_phase1.js | 22 +- .../apps/security/secure_roles_perm.js | 11 +- .../functional/apps/security/user_email.js | 2 +- x-pack/test/functional/apps/spaces/index.ts | 4 +- .../test/functional/apps/spaces/lib/types.ts | 27 - .../apps/spaces/spaces_selection.ts | 36 +- .../apps/status_page/status_page.ts | 1 + .../feature_controls/timelion_security.ts | 183 ++ .../feature_controls/timelion_spaces.ts | 137 ++ x-pack/test/functional/apps/timelion/index.ts | 17 + .../apps/uptime/feature_controls/index.ts | 14 + .../feature_controls/uptime_security.ts | 181 ++ .../uptime/feature_controls/uptime_spaces.ts | 80 + x-pack/test/functional/apps/uptime/index.ts | 1 + .../feature_controls/visualize_security.ts | 247 +++ .../feature_controls/visualize_spaces.ts | 129 ++ .../test/functional/apps/visualize/index.ts | 17 + x-pack/test/functional/config.js | 32 + .../es_archives/canvas/default/mappings.json | 3 + .../feature_controls/security/data.json | 85 + .../feature_controls/security/mappings.json | 461 +++++ .../feature_controls/spaces/data.json | 127 ++ .../feature_controls/spaces/mappings.json | 461 +++++ .../dashboard_view_mode/mappings.json | 5 +- .../feature_controls/security/data.json | 37 + .../feature_controls/security/mappings.json | 461 +++++ .../feature_controls/spaces/data.json | 77 + .../feature_controls/spaces/mappings.json | 461 +++++ .../es_archives/empty_kibana/mappings.json | 5 +- .../es_archives/logstash/empty/mappings.json | 5 +- .../es_archives/maps/kibana/mappings.json | 3 + .../feature_controls/security/data.json | 85 + .../feature_controls/security/mappings.json | 461 +++++ .../{ => security}/discover/data.json.gz | Bin .../{ => security}/discover/mappings.json | 5 +- .../spaces/disabled_features/data.json | 113 ++ .../spaces/disabled_features/mappings.json | 461 +++++ .../spaces/{ => selector}/data.json | 0 .../spaces/{ => selector}/mappings.json | 5 +- .../timelion/feature_controls/data.json | 81 + .../timelion/feature_controls/mappings.json | 461 +++++ .../es_archives/visualize/default/data.json | 110 + .../visualize/default/mappings.json | 461 +++++ .../functional/page_objects/canvas_page.ts | 44 + .../test/functional/page_objects/gis_page.js | 16 + x-pack/test/functional/page_objects/index.js | 1 + .../functional/page_objects/security_page.js | 56 +- .../saved_objects/spaces/mappings.json | 5 +- .../namespace_agnostic_type_plugin/index.js | 28 +- .../common/lib/create_users_and_roles.ts | 174 +- .../common/suites/bulk_create.ts | 2 +- .../common/suites/bulk_get.ts | 2 +- .../common/suites/create.ts | 2 +- .../common/suites/delete.ts | 2 +- .../common/suites/export.ts | 4 +- .../common/suites/find.ts | 4 +- .../common/suites/get.ts | 4 +- .../common/suites/import.ts | 6 +- .../common/suites/resolve_import_errors.ts | 6 +- .../common/suites/update.ts | 2 +- .../saved_objects/spaces/data.json | 5 +- .../saved_objects/spaces/mappings.json | 5 +- .../common/lib/authentication.ts | 16 + .../common/lib/create_users_and_roles.ts | 289 ++- .../common/suites/create.ts | 5 + .../common/suites/get.ts | 3 + .../common/suites/get_all.ts | 3 + .../common/suites/select.ts | 3 + .../common/suites/update.ts | 5 + .../security_and_spaces/apis/get_all.ts | 52 + x-pack/test/types/providers.ts | 1 + x-pack/test/types/services.d.ts | 9 + x-pack/test/typings/index.d.ts | 11 + x-pack/test/ui_capabilities/README.md | 48 + x-pack/test/ui_capabilities/common/config.ts | 57 + .../ui_capabilities/common/features.ts} | 8 +- .../fixtures/plugins/foo_plugin/index.js | 48 + .../fixtures/plugins/foo_plugin/package.json | 7 + .../fixtures/plugins/foo_plugin/public/app.js | 5 + .../common/lib/unreachable_error.ts | 11 + .../common/nav_links_builder.ts | 44 + .../saved_objects_management_builder.ts | 88 + .../common/services/features.ts | 56 + .../ui_capabilities/common/services/index.ts | 8 + .../common/services/ui_capabilities.ts | 114 ++ x-pack/test/ui_capabilities/common/types.ts | 56 + .../security_and_spaces/config.ts | 10 + .../security_and_spaces/scenarios.ts | 468 +++++ .../security_and_spaces/tests/catalogue.ts | 92 + .../security_and_spaces/tests/foo.ts | 96 + .../security_and_spaces/tests/index.ts | 78 + .../security_and_spaces/tests/nav_links.ts | 85 + .../tests/saved_objects_management.ts | 114 ++ .../ui_capabilities/security_only/config.ts | 10 + .../security_only/scenarios.ts | 215 ++ .../security_only/tests/catalogue.ts | 74 + .../security_only/tests/foo.ts | 67 + .../security_only/tests/index.ts | 60 + .../security_only/tests/nav_links.ts | 70 + .../tests/saved_objects_management.ts | 115 ++ .../ui_capabilities/spaces_only/config.ts | 10 + .../ui_capabilities/spaces_only/scenarios.ts | 44 + .../spaces_only/tests/catalogue.ts | 55 + .../ui_capabilities/spaces_only/tests/foo.ts | 48 + .../spaces_only/tests/index.ts | 46 + .../spaces_only/tests/nav_links.ts | 51 + .../tests/saved_objects_management.ts | 36 + 735 files changed, 44866 insertions(+), 9778 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md create mode 100644 docs/development/core/public/kibana-plugin-public.capabilities.management.md create mode 100644 docs/development/core/public/kibana-plugin-public.capabilities.md create mode 100644 docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md create mode 100644 docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md create mode 100644 docs/development/core/public/kibana-plugin-public.capabilitiessetup.md create mode 100644 docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md create mode 100644 src/core/public/capabilities/capabilities_service.mock.ts create mode 100644 src/core/public/capabilities/capabilities_service.test.ts create mode 100644 src/core/public/capabilities/capabilities_service.tsx create mode 100644 src/core/public/capabilities/index.ts rename src/core/public/{injected_metadata => utils}/deep_freeze.test.ts (100%) rename src/core/public/{injected_metadata => utils}/deep_freeze.ts (100%) rename src/core/public/{injected_metadata => utils}/integration_tests/__fixtures__/frozen_object_mutation/index.ts (100%) rename src/core/public/{injected_metadata => utils}/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json (100%) rename src/core/public/{injected_metadata => utils}/integration_tests/deep_freeze.test.ts (100%) delete mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/get_in_app_url.test.js create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/__jest__/in_app_url.test.js rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{get_in_app_url.js => in_app_url.js} (70%) create mode 100644 src/legacy/ui/public/capabilities/index.ts create mode 100644 src/legacy/ui/public/capabilities/react/index.ts create mode 100644 src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx create mode 100644 src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx create mode 100644 src/legacy/ui/public/capabilities/react/legacy/index.ts create mode 100644 src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx create mode 100644 src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx create mode 100644 src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx create mode 100644 src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts create mode 100644 src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx create mode 100644 src/legacy/ui/public/capabilities/route_setup.ts rename src/legacy/ui/public/management/{__tests__/section.js => section.test.js} (70%) create mode 100644 src/legacy/ui/public/registry/feature_catalogue.test.js create mode 100644 test/functional/page_objects/error_page.js create mode 100644 x-pack/plugins/__mocks__/ui/capabilities.ts rename x-pack/plugins/security/common/{constants.js => constants.ts} (77%) rename x-pack/plugins/security/common/model/{index_privilege.ts => features_privileges.ts} (63%) create mode 100644 x-pack/plugins/security/common/model/index.ts create mode 100644 x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts create mode 100644 x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts create mode 100644 x-pack/plugins/security/common/model/kibana_privileges/index.ts create mode 100644 x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts create mode 100644 x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts create mode 100644 x-pack/plugins/security/common/model/raw_kibana_privileges.ts create mode 100644 x-pack/plugins/security/common/privilege_calculator_utils.test.ts create mode 100644 x-pack/plugins/security/common/privilege_calculator_utils.ts create mode 100644 x-pack/plugins/security/public/components/_index.scss create mode 100644 x-pack/plugins/security/public/components/management/users/_index.scss create mode 100644 x-pack/plugins/security/public/components/management/users/_users.scss create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts create mode 100644 x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts create mode 100644 x-pack/plugins/security/public/lib/privilege_utils.test.ts create mode 100644 x-pack/plugins/security/public/lib/privilege_utils.ts rename x-pack/plugins/security/public/lib/{role.test.ts => role_utils.test.ts} (51%) rename x-pack/plugins/security/public/lib/{role.ts => role_utils.ts} (59%) create mode 100644 x-pack/plugins/security/public/lib/transform_role_for_save.test.ts create mode 100644 x-pack/plugins/security/public/lib/transform_role_for_save.ts create mode 100644 x-pack/plugins/security/public/views/_index.scss create mode 100644 x-pack/plugins/security/public/views/management/_index.scss create mode 100644 x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss create mode 100644 x-pack/plugins/security/public/views/management/change_password_form/_index.scss create mode 100644 x-pack/plugins/security/public/views/management/edit_role/_index.scss create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/_index.scss rename x-pack/plugins/security/public/views/management/edit_role/components/{ => collapsible_panel}/__snapshots__/collapsible_panel.test.tsx.snap (100%) rename x-pack/plugins/security/public/views/management/edit_role/components/{collapsible_panel.scss => collapsible_panel/_collapsible_panel.scss} (100%) rename x-pack/plugins/security/public/views/management/edit_role/components/{ => collapsible_panel}/collapsible_panel.test.tsx (100%) rename x-pack/plugins/security/public/views/management/edit_role/components/{ => collapsible_panel}/collapsible_panel.tsx (76%) create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/impacted_spaces_flyout.test.tsx.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_callout_warning.test.tsx.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/privilege_space_form.test.tsx.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/simple_privilege_form.test.tsx.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_selector.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/index.ts delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.scss delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_callout_warning.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_form.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/privilege_space_table.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_form.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts rename x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/{ => simple_privilege_section}/privilege_selector.tsx (79%) create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_selector.test.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_selector.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts create mode 100644 x-pack/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/lib/__snapshots__/get_available_privileges.test.ts.snap delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.test.ts delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/lib/get_available_privileges.test.ts delete mode 100644 x-pack/plugins/security/public/views/management/edit_role/lib/get_available_privileges.ts delete mode 100644 x-pack/plugins/security/public/views/management/index.scss 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__/actions.test.js.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 create mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/privileges_serializer.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/resource_serializer.test.ts.snap delete mode 100644 x-pack/plugins/security/server/lib/authorization/__snapshots__/space_application_privileges_serializer.test.js.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/actions.js delete mode 100644 x-pack/plugins/security/server/lib/authorization/actions.test.js create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/__snapshots__/api.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/__snapshots__/app.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/__snapshots__/saved_object.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/__snapshots__/ui.test.ts.snap create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/actions.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/actions.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/api.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/api.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/app.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/app.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/index.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/saved_object.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/saved_object.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/space.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/space.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/ui.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/actions/ui.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/api_authorization.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/api_authorization.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/app_authorization.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/app_authorization.ts 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 delete mode 100644 x-pack/plugins/security/server/lib/authorization/index.js create mode 100644 x-pack/plugins/security/server/lib/authorization/index.ts delete mode 100644 x-pack/plugins/security/server/lib/authorization/mode.js rename x-pack/plugins/security/server/lib/authorization/{mode.test.js => mode.test.ts} (76%) create mode 100644 x-pack/plugins/security/server/lib/authorization/mode.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privilege_serializer.ts delete mode 100644 x-pack/plugins/security/server/lib/authorization/privileges.js delete mode 100644 x-pack/plugins/security/server/lib/authorization/privileges.test.js create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/api.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/app.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/catalogue.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/index.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/management.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/navlink.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/saved_object.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/saved_objects_management.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/feature_privilege_builder/ui.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/index.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/privileges.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges/privileges.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges_serializer.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/privileges_serializer.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/resource_serializer.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/resource_serializer.ts delete mode 100644 x-pack/plugins/security/server/lib/authorization/service.js delete mode 100644 x-pack/plugins/security/server/lib/authorization/service.test.js create mode 100644 x-pack/plugins/security/server/lib/authorization/service.test.mocks.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/service.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/service.ts delete mode 100644 x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.js delete mode 100644 x-pack/plugins/security/server/lib/authorization/space_application_privileges_serializer.test.js 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} (70%) 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/authorization/validate_feature_privileges.test.ts create mode 100644 x-pack/plugins/security/server/lib/authorization/validate_feature_privileges.ts 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 create mode 100644 x-pack/plugins/security/server/routes/api/public/privileges/get.test.ts create mode 100644 x-pack/plugins/security/server/routes/api/public/privileges/get.ts create mode 100644 x-pack/plugins/security/server/routes/api/public/privileges/index.ts delete mode 100644 x-pack/plugins/spaces/public/views/management/_manage_spaces.scss delete mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/__snapshots__/customize_space_avatar.test.tsx.snap create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/__snapshots__/confirm_alter_active_space_modal.test.tsx.snap create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.test.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/confirm_alter_active_space_modal.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/confirm_alter_active_space_modal/index.ts create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap rename x-pack/plugins/spaces/public/views/management/edit_space/{ => customize_space}/__snapshots__/space_identifier.test.tsx.snap (53%) create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/customize_space/customize_space.tsx rename x-pack/plugins/spaces/public/views/management/edit_space/{ => customize_space}/customize_space_avatar.test.tsx (67%) rename x-pack/plugins/spaces/public/views/management/edit_space/{ => customize_space}/customize_space_avatar.tsx (75%) create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/customize_space/index.ts rename x-pack/plugins/spaces/public/views/management/edit_space/{ => customize_space}/space_identifier.test.tsx (87%) rename x-pack/plugins/spaces/public/views/management/edit_space/{ => customize_space}/space_identifier.tsx (56%) create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/_index.scss create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.test.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/enabled_features.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/feature_table.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/index.ts create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/enabled_features/toggle_all_features.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/section_panel/__snapshots__/section_panel.test.tsx.snap create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/section_panel/_section_panel.scss create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/section_panel/index.ts create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.test.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/edit_space/section_panel/section_panel.tsx create mode 100644 x-pack/plugins/spaces/public/views/management/lib/feature_utils.test.ts create mode 100644 x-pack/plugins/spaces/public/views/management/lib/feature_utils.ts create mode 100644 x-pack/plugins/spaces/server/lib/migrations/index.ts create mode 100644 x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts create mode 100644 x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts create mode 100644 x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts rename x-pack/plugins/spaces/server/lib/{space_request_interceptors.ts => request_inteceptors/on_post_auth_interceptor.ts} (52%) create mode 100644 x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.ts delete mode 100644 x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts create mode 100644 x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts rename x-pack/plugins/{security/public/views/management/edit_role/lib/copy_role.ts => spaces/types.d.ts} (60%) delete mode 100644 x-pack/plugins/xpack_main/public/services/user_profile.test.ts delete mode 100644 x-pack/plugins/xpack_main/public/services/user_profile.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap create mode 100644 x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/feature_registry/index.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts create mode 100644 x-pack/plugins/xpack_main/server/lib/register_oss_features.ts delete mode 100644 x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts delete mode 100644 x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts create mode 100644 x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap create mode 100644 x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts create mode 100644 x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts create mode 100644 x-pack/plugins/xpack_main/server/routes/api/v1/features/index.ts create mode 100644 x-pack/plugins/xpack_main/types.ts create mode 100644 x-pack/test/api_integration/apis/apm/feature_controls.ts create mode 100644 x-pack/test/api_integration/apis/apm/index.ts create mode 100644 x-pack/test/api_integration/apis/console/feature_controls.ts create mode 100644 x-pack/test/api_integration/apis/console/index.ts create mode 100644 x-pack/test/api_integration/apis/infra/feature_controls.ts create mode 100644 x-pack/test/api_integration/apis/security/privileges.ts create mode 100644 x-pack/test/api_integration/apis/uptime/constants.ts create mode 100644 x-pack/test/api_integration/apis/uptime/feature_controls.ts create mode 100644 x-pack/test/api_integration/apis/xpack_main/features/features.ts create mode 100644 x-pack/test/api_integration/apis/xpack_main/features/index.ts create mode 100644 x-pack/test/common/services/index.ts create mode 100644 x-pack/test/common/services/security/index.ts create mode 100644 x-pack/test/common/services/security/role.ts create mode 100644 x-pack/test/common/services/security/user.ts create mode 100644 x-pack/test/common/services/spaces/index.ts create mode 100644 x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts create mode 100644 x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts create mode 100644 x-pack/test/functional/apps/advanced_settings/index.ts create mode 100644 x-pack/test/functional/apps/apm/feature_controls/apm_security.ts create mode 100644 x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts create mode 100644 x-pack/test/functional/apps/apm/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/apm/index.ts create mode 100644 x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts create mode 100644 x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts create mode 100644 x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts create mode 100644 x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts create mode 100644 x-pack/test/functional/apps/dashboard/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/dashboard/index.ts create mode 100644 x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts create mode 100644 x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts create mode 100644 x-pack/test/functional/apps/dev_tools/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/dev_tools/index.ts create mode 100644 x-pack/test/functional/apps/discover/feature_controls/discover_security.ts create mode 100644 x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts create mode 100644 x-pack/test/functional/apps/discover/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/discover/index.ts create mode 100644 x-pack/test/functional/apps/graph/feature_controls/graph_security.ts create mode 100644 x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts create mode 100644 x-pack/test/functional/apps/graph/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/index_patterns/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts create mode 100644 x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts create mode 100644 x-pack/test/functional/apps/index_patterns/index.ts create mode 100644 x-pack/test/functional/apps/infra/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts create mode 100644 x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts create mode 100644 x-pack/test/functional/apps/infra/feature_controls/logs_security.ts create mode 100644 x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts create mode 100644 x-pack/test/functional/apps/machine_learning/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts create mode 100644 x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts create mode 100644 x-pack/test/functional/apps/machine_learning/index.ts create mode 100644 x-pack/test/functional/apps/maps/feature_controls/maps_security.ts create mode 100644 x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts create mode 100644 x-pack/test/functional/apps/monitoring/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts create mode 100644 x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts create mode 100644 x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts create mode 100644 x-pack/test/functional/apps/saved_objects_management/index.ts delete mode 100644 x-pack/test/functional/apps/spaces/lib/types.ts create mode 100644 x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts create mode 100644 x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts create mode 100644 x-pack/test/functional/apps/timelion/index.ts create mode 100644 x-pack/test/functional/apps/uptime/feature_controls/index.ts create mode 100644 x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts create mode 100644 x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts create mode 100644 x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts create mode 100644 x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts create mode 100644 x-pack/test/functional/apps/visualize/index.ts create mode 100644 x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json create mode 100644 x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json create mode 100644 x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json create mode 100644 x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json create mode 100644 x-pack/test/functional/es_archives/discover/feature_controls/security/data.json create mode 100644 x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json create mode 100644 x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json create mode 100644 x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json create mode 100644 x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json create mode 100644 x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json rename x-pack/test/functional/es_archives/{ => security}/discover/data.json.gz (100%) rename x-pack/test/functional/es_archives/{ => security}/discover/mappings.json (98%) create mode 100644 x-pack/test/functional/es_archives/spaces/disabled_features/data.json create mode 100644 x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json rename x-pack/test/functional/es_archives/spaces/{ => selector}/data.json (100%) rename x-pack/test/functional/es_archives/spaces/{ => selector}/mappings.json (98%) create mode 100644 x-pack/test/functional/es_archives/timelion/feature_controls/data.json create mode 100644 x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json create mode 100644 x-pack/test/functional/es_archives/visualize/default/data.json create mode 100644 x-pack/test/functional/es_archives/visualize/default/mappings.json create mode 100644 x-pack/test/functional/page_objects/canvas_page.ts create mode 100644 x-pack/test/types/services.d.ts create mode 100644 x-pack/test/typings/index.d.ts create mode 100644 x-pack/test/ui_capabilities/README.md create mode 100644 x-pack/test/ui_capabilities/common/config.ts rename x-pack/{plugins/security/common/model/kibana_privilege.ts => test/ui_capabilities/common/features.ts} (65%) create mode 100644 x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js create mode 100644 x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json create mode 100644 x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js create mode 100644 x-pack/test/ui_capabilities/common/lib/unreachable_error.ts create mode 100644 x-pack/test/ui_capabilities/common/nav_links_builder.ts create mode 100644 x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts create mode 100644 x-pack/test/ui_capabilities/common/services/features.ts create mode 100644 x-pack/test/ui_capabilities/common/services/index.ts create mode 100644 x-pack/test/ui_capabilities/common/services/ui_capabilities.ts create mode 100644 x-pack/test/ui_capabilities/common/types.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/config.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts create mode 100644 x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts create mode 100644 x-pack/test/ui_capabilities/security_only/config.ts create mode 100644 x-pack/test/ui_capabilities/security_only/scenarios.ts create mode 100644 x-pack/test/ui_capabilities/security_only/tests/catalogue.ts create mode 100644 x-pack/test/ui_capabilities/security_only/tests/foo.ts create mode 100644 x-pack/test/ui_capabilities/security_only/tests/index.ts create mode 100644 x-pack/test/ui_capabilities/security_only/tests/nav_links.ts create mode 100644 x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/config.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/scenarios.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/tests/foo.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/tests/index.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts create mode 100644 x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md b/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md new file mode 100644 index 00000000000000..26d4afaec0ccbf --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.catalogue.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [catalogue](./kibana-plugin-public.capabilities.catalogue.md) + +## Capabilities.catalogue property + +Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. + +Signature: + +```typescript +catalogue: Record; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.management.md b/docs/development/core/public/kibana-plugin-public.capabilities.management.md new file mode 100644 index 00000000000000..d225f1c2ba740e --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.management.md @@ -0,0 +1,13 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [management](./kibana-plugin-public.capabilities.management.md) + +## Capabilities.management property + +Management section capabilities. + +Signature: + +```typescript +management: { + [sectionId: string]: Record; + }; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.md b/docs/development/core/public/kibana-plugin-public.capabilities.md new file mode 100644 index 00000000000000..7ea53e248160fe --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.md @@ -0,0 +1,20 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) + +## Capabilities interface + +The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. + +Signature: + +```typescript +export interface Capabilities +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [catalogue](./kibana-plugin-public.capabilities.catalogue.md) | Record<string, boolean> | Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. | +| [management](./kibana-plugin-public.capabilities.management.md) | {`

` [sectionId: string]: Record<string, boolean>;`

` } | Management section capabilities. | +| [navLinks](./kibana-plugin-public.capabilities.navlinks.md) | Record<string, boolean> | Navigation link capabilities. | + diff --git a/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md b/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md new file mode 100644 index 00000000000000..b837dbd5dd541a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilities.navlinks.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [Capabilities](./kibana-plugin-public.capabilities.md) > [navLinks](./kibana-plugin-public.capabilities.navlinks.md) + +## Capabilities.navLinks property + +Navigation link capabilities. + +Signature: + +```typescript +navLinks: Record; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md new file mode 100644 index 00000000000000..43fcaacaab0412 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.getcapabilities.md @@ -0,0 +1,11 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) > [getCapabilities](./kibana-plugin-public.capabilitiessetup.getcapabilities.md) + +## CapabilitiesSetup.getCapabilities property + +Gets the read-only capabilities. + +Signature: + +```typescript +getCapabilities: () => Capabilities; +``` diff --git a/docs/development/core/public/kibana-plugin-public.capabilitiessetup.md b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.md new file mode 100644 index 00000000000000..ab5b239e587cc9 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.capabilitiessetup.md @@ -0,0 +1,18 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) + +## CapabilitiesSetup interface + +Capabilities Setup. + +Signature: + +```typescript +export interface CapabilitiesSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getCapabilities](./kibana-plugin-public.capabilitiessetup.getcapabilities.md) | () => Capabilities | Gets the read-only capabilities. | + diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md b/docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md new file mode 100644 index 00000000000000..5e1863af09c932 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-public.coresetup.capabilities.md @@ -0,0 +1,9 @@ +[Home](./index) > [kibana-plugin-public](./kibana-plugin-public.md) > [CoreSetup](./kibana-plugin-public.coresetup.md) > [capabilities](./kibana-plugin-public.coresetup.capabilities.md) + +## CoreSetup.capabilities property + +Signature: + +```typescript +capabilities: CapabilitiesSetup; +``` diff --git a/docs/development/core/public/kibana-plugin-public.coresetup.md b/docs/development/core/public/kibana-plugin-public.coresetup.md index a34c13b944125b..13384b87d5d82d 100644 --- a/docs/development/core/public/kibana-plugin-public.coresetup.md +++ b/docs/development/core/public/kibana-plugin-public.coresetup.md @@ -15,6 +15,7 @@ export interface CoreSetup | Property | Type | Description | | --- | --- | --- | | [basePath](./kibana-plugin-public.coresetup.basepath.md) | BasePathSetup | | +| [capabilities](./kibana-plugin-public.coresetup.capabilities.md) | CapabilitiesSetup | | | [chrome](./kibana-plugin-public.coresetup.chrome.md) | ChromeSetup | | | [fatalErrors](./kibana-plugin-public.coresetup.fatalerrors.md) | FatalErrorsSetup | | | [http](./kibana-plugin-public.coresetup.http.md) | HttpSetup | | diff --git a/docs/development/core/public/kibana-plugin-public.md b/docs/development/core/public/kibana-plugin-public.md index 372592c9f5817f..485eb4a0700a10 100644 --- a/docs/development/core/public/kibana-plugin-public.md +++ b/docs/development/core/public/kibana-plugin-public.md @@ -14,6 +14,8 @@ | Interface | Description | | --- | --- | +| [Capabilities](./kibana-plugin-public.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | +| [CapabilitiesSetup](./kibana-plugin-public.capabilitiessetup.md) | Capabilities Setup. | | [ChromeBrand](./kibana-plugin-public.chromebrand.md) | | | [ChromeBreadcrumb](./kibana-plugin-public.chromebreadcrumb.md) | | | [CoreSetup](./kibana-plugin-public.coresetup.md) | Core services exposed to the start lifecycle | diff --git a/docs/development/security/rbac.asciidoc b/docs/development/security/rbac.asciidoc index 916ec72ab76c7b..6feb499f588713 100644 --- a/docs/development/security/rbac.asciidoc +++ b/docs/development/security/rbac.asciidoc @@ -32,9 +32,9 @@ Authorization: Basic kibana changeme "actions":[ "version:7.0.0-alpha1-SNAPSHOT", "action:login", - "action:saved_objects/dashboard/get", - "action:saved_objects/dashboard/bulk_get", - "action:saved_objects/dashboard/find", + "saved_object:dashboard/get", + "saved_object:dashboard/bulk_get", + "saved_object:dashboard/find", ... ],"metadata":{}} } @@ -90,7 +90,7 @@ Authorization: Basic foo_read_only_user password "application":"kibana-.kibana", "resources":["*"], "privileges":[ - "action:saved_objects/dashboard/save", + "saved_object:dashboard/save", ] } ] @@ -120,7 +120,7 @@ Authorization: Basic foo_legacy_user password "application":"kibana-.kibana", "resources":["*"], "privileges":[ - "action:saved_objects/dashboard/save" + "saved_object:dashboard/save" ] } ], @@ -152,7 +152,7 @@ Here is an example response if the user does not have application privileges, bu "application": { "kibana-.kibana": { "*": { - "action:saved_objects/dashboard/save": false + "saved_object:dashboard/save": false } } } diff --git a/packages/kbn-plugin-generator/sao_template/sao.test.js b/packages/kbn-plugin-generator/sao_template/sao.test.js index 7d12041cf02578..77c9b4533b9ca2 100755 --- a/packages/kbn-plugin-generator/sao_template/sao.test.js +++ b/packages/kbn-plugin-generator/sao_template/sao.test.js @@ -55,6 +55,7 @@ describe('plugin generator sao integration', () => { expect(uiExports).not.toContain('app:'); expect(uiExports).not.toContain('hacks:'); expect(uiExports).not.toContain('init(server, options)'); + expect(uiExports).not.toContain('registerFeature('); }); it('includes app when answering yes', async () => { @@ -73,8 +74,9 @@ describe('plugin generator sao integration', () => { const uiExports = getConfig(res.files['index.js']); expect(uiExports).toContain('app:'); + expect(uiExports).toContain('init(server, options)'); + expect(uiExports).toContain('registerFeature('); expect(uiExports).not.toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); }); it('includes hack when answering yes', async () => { @@ -94,7 +96,8 @@ describe('plugin generator sao integration', () => { const uiExports = getConfig(res.files['index.js']); expect(uiExports).toContain('app:'); expect(uiExports).toContain('hacks:'); - expect(uiExports).not.toContain('init(server, options)'); + expect(uiExports).toContain('init(server, options)'); + expect(uiExports).toContain('registerFeature('); }); it('includes server api when answering yes', async () => { @@ -115,6 +118,7 @@ describe('plugin generator sao integration', () => { expect(uiExports).toContain('app:'); expect(uiExports).toContain('hacks:'); expect(uiExports).toContain('init(server, options)'); + expect(uiExports).toContain('registerFeature('); }); it('plugin config has correct name and main path', async () => { diff --git a/packages/kbn-plugin-generator/sao_template/template/index.js b/packages/kbn-plugin-generator/sao_template/template/index.js index 788ea2d3e33831..8377ff8d074c65 100755 --- a/packages/kbn-plugin-generator/sao_template/template/index.js +++ b/packages/kbn-plugin-generator/sao_template/template/index.js @@ -3,6 +3,11 @@ import { resolve } from 'path'; import { existsSync } from 'fs'; <% } -%> + +<% if (generateApp) { -%> +import { i18n } from '@kbn/i18n'; +<% } -%> + <% if (generateApi) { -%> import exampleRoute from './server/routes/example'; @@ -34,11 +39,49 @@ export default function (kibana) { enabled: Joi.boolean().default(true), }).default(); }, - <%_ if (generateApi) { -%> + <%_ if (generateApi || generateApp) { -%> init(server, options) { // eslint-disable-line no-unused-vars + <%_ if (generateApp) { -%> + const xpackMainPlugin = server.plugins.xpack_main; + if (xpackMainPlugin) { + const featureId = '<%= snakeCase(name) %>'; + + xpackMainPlugin.registerFeature({ + id: featureId, + name: i18n.translate('<%= camelCase(name) %>.featureRegistry.featureName', { + defaultMessage: '<%= name %>', + }), + navLinkId: featureId, + icon: 'questionInCircle', + app: [featureId, 'kibana'], + catalogue: [], + privileges: { + all: { + api: [], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + read: { + api: [], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + }, + }); + } + <%_ } -%> + + <%_ if (generateApi) { -%> // Add server routes and initialize the plugin here exampleRoute(server); + <%_ } -%> } <%_ } -%> }); diff --git a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js index ad15d94b345c2a..ba3bf91fcdf116 100644 --- a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js +++ b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table.js @@ -47,6 +47,7 @@ export function KuiListingTable({ toolBarActions, onFilter, onItemSelectionChanged, + enableSelection, selectedRowIds, filter, prompt, @@ -76,11 +77,12 @@ export function KuiListingTable({ } } - function renderTableRows() { + function renderTableRows(enableSelection) { return rows.map((row, rowIndex) => { return ( = 0} onSelectionChanged={toggleRow} row={row} @@ -111,15 +113,17 @@ export function KuiListingTable({ return ( - + {enableSelection && + + } {renderHeader()} - {renderTableRows()} + {renderTableRows(enableSelection)} ); @@ -171,6 +175,7 @@ KuiListingTable.propTypes = { })), pager: PropTypes.node, onItemSelectionChanged: PropTypes.func.isRequired, + enableSelection: PropTypes.bool, selectedRowIds: PropTypes.array, prompt: PropTypes.node, // If given, will be shown instead of a table with rows. onFilter: PropTypes.func, @@ -181,4 +186,5 @@ KuiListingTable.propTypes = { KuiListingTable.defaultProps = { rows: [], selectedRowIds: [], + enableSelection: true, }; diff --git a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js index 212ccfd7036371..f332e35c9b74c7 100644 --- a/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js +++ b/packages/kbn-ui-framework/src/components/table/listing_table/listing_table_row.js @@ -57,13 +57,15 @@ export class KuiListingTableRow extends React.PureComponent { } render() { - const { isSelected } = this.props; + const { enableSelection, isSelected } = this.props; return ( - + {enableSelection && + + } {this.renderCells()} ); @@ -83,6 +85,11 @@ KuiListingTableRow.propTypes = { ], )), }).isRequired, + enableSelection: PropTypes.bool, onSelectionChanged: PropTypes.func.isRequired, isSelected: PropTypes.bool, }; + +KuiListingTableRow.defaultProps = { + enableSelection: true, +}; diff --git a/src/core/public/capabilities/capabilities_service.mock.ts b/src/core/public/capabilities/capabilities_service.mock.ts new file mode 100644 index 00000000000000..980755302f3830 --- /dev/null +++ b/src/core/public/capabilities/capabilities_service.mock.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + getCapabilities: jest.fn(), + }; + setupContract.getCapabilities.mockReturnValue({ + catalogue: {}, + management: {}, + navLinks: {}, + } as Capabilities); + return setupContract; +}; + +type CapabilitiesServiceContract = PublicMethodsOf; +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn(), + }; + mocked.setup.mockReturnValue(createSetupContractMock()); + return mocked; +}; + +export const capabilitiesServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/public/capabilities/capabilities_service.test.ts b/src/core/public/capabilities/capabilities_service.test.ts new file mode 100644 index 00000000000000..9c138b780f5631 --- /dev/null +++ b/src/core/public/capabilities/capabilities_service.test.ts @@ -0,0 +1,60 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { InjectedMetadataService } from '../injected_metadata'; +import { CapabilitiesService } from './capabilities_service'; + +describe('#start', () => { + it('returns a service with getCapabilities', () => { + const injectedMetadata = new InjectedMetadataService({ + injectedMetadata: { + vars: { + uiCapabilities: { + foo: 'bar', + bar: 'baz', + }, + }, + } as any, + }); + const service = new CapabilitiesService(); + const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() }); + expect(startContract.getCapabilities()).toEqual({ + foo: 'bar', + bar: 'baz', + }); + }); + + it(`does not allow Capabilities to be modified`, () => { + const injectedMetadata = new InjectedMetadataService({ + injectedMetadata: { + vars: { + uiCapabilities: { + foo: 'bar', + bar: 'baz', + }, + }, + } as any, + }); + const service = new CapabilitiesService(); + const startContract = service.setup({ injectedMetadata: injectedMetadata.setup() }); + const capabilities = startContract.getCapabilities(); + + // @ts-ignore TypeScript knows this shouldn't be possible + expect(() => (capabilities.foo = 'foo')).toThrowError(); + }); +}); diff --git a/src/core/public/capabilities/capabilities_service.tsx b/src/core/public/capabilities/capabilities_service.tsx new file mode 100644 index 00000000000000..560b8f628ee37f --- /dev/null +++ b/src/core/public/capabilities/capabilities_service.tsx @@ -0,0 +1,72 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { InjectedMetadataSetup } from '../injected_metadata'; +import { deepFreeze } from '../utils/deep_freeze'; + +interface StartDeps { + injectedMetadata: InjectedMetadataSetup; +} + +/** + * The read-only set of capabilities available for the current UI session. + * Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, + * and the boolean is a flag indicating if the capability is enabled or disabled. + * + * @public + */ +export interface Capabilities { + /** Navigation link capabilities. */ + navLinks: Record; + + /** Management section capabilities. */ + management: { + [sectionId: string]: Record; + }; + + /** Catalogue capabilities. Catalogue entries drive the visibility of the Kibana homepage options. */ + catalogue: Record; + + /** Custom capabilities, registered by plugins. */ + [key: string]: Record>; +} + +/** + * Capabilities Setup. + * @public + */ +export interface CapabilitiesSetup { + /** + * Gets the read-only capabilities. + */ + getCapabilities: () => Capabilities; +} + +/** @internal */ + +/** + * Service that is responsible for UI Capabilities. + */ +export class CapabilitiesService { + public setup({ injectedMetadata }: StartDeps): CapabilitiesSetup { + return { + getCapabilities: () => + deepFreeze(injectedMetadata.getInjectedVar('uiCapabilities') as Capabilities), + }; + } +} diff --git a/src/core/public/capabilities/index.ts b/src/core/public/capabilities/index.ts new file mode 100644 index 00000000000000..dd95d8530a5bd3 --- /dev/null +++ b/src/core/public/capabilities/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { Capabilities, CapabilitiesService, CapabilitiesSetup } from './capabilities_service'; diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 8d3bf01322342e..9cce855397271a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -23,6 +23,7 @@ import { Subject } from 'rxjs'; import { CoreSetup } from '.'; import { BasePathService } from './base_path'; +import { CapabilitiesService } from './capabilities'; import { ChromeService } from './chrome'; import { FatalErrorsService } from './fatal_errors'; import { HttpService } from './http'; @@ -64,6 +65,7 @@ export class CoreSystem { private readonly basePath: BasePathService; private readonly chrome: ChromeService; private readonly i18n: I18nService; + private readonly capabilities: CapabilitiesService; private readonly overlay: OverlayService; private readonly plugins: PluginsService; @@ -85,6 +87,8 @@ export class CoreSystem { this.i18n = new I18nService(); + this.capabilities = new CapabilitiesService(); + this.injectedMetadata = new InjectedMetadataService({ injectedMetadata, }); @@ -128,6 +132,7 @@ export class CoreSystem { const http = this.http.setup({ fatalErrors }); const overlays = this.overlay.setup({ i18n }); const basePath = this.basePath.setup({ injectedMetadata }); + const capabilities = this.capabilities.setup({ injectedMetadata }); const uiSettings = this.uiSettings.setup({ notifications, http, @@ -145,6 +150,7 @@ export class CoreSystem { fatalErrors, http, i18n, + capabilities, injectedMetadata, notifications, uiSettings, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 380d0ebbdbf2d7..90dfe77b9eb65b 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -18,6 +18,7 @@ */ import { BasePathSetup } from './base_path'; +import { Capabilities, CapabilitiesSetup } from './capabilities'; import { ChromeBrand, ChromeBreadcrumb, ChromeHelpExtension, ChromeSetup } from './chrome'; import { FatalErrorsSetup } from './fatal_errors'; import { HttpSetup } from './http'; @@ -43,6 +44,7 @@ export interface CoreSetup { notifications: NotificationsSetup; http: HttpSetup; basePath: BasePathSetup; + capabilities: CapabilitiesSetup; uiSettings: UiSettingsSetup; chrome: ChromeSetup; overlays: OverlaySetup; @@ -53,6 +55,8 @@ export { HttpSetup, FatalErrorsSetup, I18nSetup, + CapabilitiesSetup, + Capabilities, ChromeSetup, ChromeBreadcrumb, ChromeBrand, diff --git a/src/core/public/injected_metadata/injected_metadata_service.ts b/src/core/public/injected_metadata/injected_metadata_service.ts index 85001fbd3de349..b7b76f195e74ae 100644 --- a/src/core/public/injected_metadata/injected_metadata_service.ts +++ b/src/core/public/injected_metadata/injected_metadata_service.ts @@ -20,7 +20,7 @@ import { get } from 'lodash'; import { DiscoveredPlugin, PluginName } from '../../server'; import { UiSettingsState } from '../ui_settings'; -import { deepFreeze } from './deep_freeze'; +import { deepFreeze } from '../utils/deep_freeze'; /** @internal */ export interface InjectedMetadataParams { diff --git a/src/core/public/kibana.api.md b/src/core/public/kibana.api.md index 9dca4df6a0c580..85d4d86fda4ae2 100644 --- a/src/core/public/kibana.api.md +++ b/src/core/public/kibana.api.md @@ -16,6 +16,21 @@ import { Toast } from '@elastic/eui'; // @public (undocumented) export type BasePathSetup = ReturnType; +// @public +export interface Capabilities { + [key: string]: Record>; + catalogue: Record; + management: { + [sectionId: string]: Record; + }; + navLinks: Record; +} + +// @public +export interface CapabilitiesSetup { + getCapabilities: () => Capabilities; +} + // @public (undocumented) export interface ChromeBrand { // (undocumented) @@ -53,6 +68,8 @@ export interface CoreSetup { // (undocumented) basePath: BasePathSetup; // (undocumented) + capabilities: CapabilitiesSetup; + // (undocumented) chrome: ChromeSetup; // (undocumented) fatalErrors: FatalErrorsSetup; diff --git a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap index c1af66257c674d..096ef3addfedd8 100644 --- a/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap +++ b/src/core/public/legacy/__snapshots__/legacy_service.test.ts.snap @@ -6,6 +6,7 @@ Array [ "ui/i18n", "ui/notify/fatal_error", "ui/notify/toasts", + "ui/capabilities", "ui/chrome/api/loading_count", "ui/chrome/api/base_path", "ui/chrome/api/ui_settings", @@ -26,6 +27,7 @@ Array [ "ui/i18n", "ui/notify/fatal_error", "ui/notify/toasts", + "ui/capabilities", "ui/chrome/api/loading_count", "ui/chrome/api/base_path", "ui/chrome/api/ui_settings", diff --git a/src/core/public/legacy/legacy_service.test.ts b/src/core/public/legacy/legacy_service.test.ts index 1aadf271f5e549..7ee06f76b80df5 100644 --- a/src/core/public/legacy/legacy_service.test.ts +++ b/src/core/public/legacy/legacy_service.test.ts @@ -53,6 +53,14 @@ jest.mock('ui/i18n', () => { }; }); +const mockUICapabilitiesInit = jest.fn(); +jest.mock('ui/capabilities', () => { + mockLoadOrder.push('ui/capabilities'); + return { + __newPlatformInit__: mockUICapabilitiesInit, + }; +}); + const mockFatalErrorInit = jest.fn(); jest.mock('ui/notify/fatal_error', () => { mockLoadOrder.push('ui/notify/fatal_error'); @@ -142,6 +150,7 @@ jest.mock('ui/chrome/services/global_nav_state', () => { }); import { basePathServiceMock } from '../base_path/base_path_service.mock'; +import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; import { chromeServiceMock } from '../chrome/chrome_service.mock'; import { fatalErrorsServiceMock } from '../fatal_errors/fatal_errors_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; @@ -159,6 +168,7 @@ const httpSetup = httpServiceMock.createSetupContract(); const i18nSetup = i18nServiceMock.createSetupContract(); const injectedMetadataSetup = injectedMetadataServiceMock.createSetupContract(); const notificationsSetup = notificationServiceMock.createSetupContract(); +const capabilitiesSetup = capabilitiesServiceMock.createSetupContract(); const uiSettingsSetup = uiSettingsServiceMock.createSetupContract(); const overlaySetup = overlayServiceMock.createSetupContract(); @@ -176,6 +186,7 @@ const defaultSetupDeps = { notifications: notificationsSetup, http: httpSetup, basePath: basePathSetup, + capabilities: capabilitiesSetup, uiSettings: uiSettingsSetup, chrome: chromeSetup, overlays: overlaySetup, @@ -215,6 +226,17 @@ describe('#setup()', () => { expect(mockI18nContextInit).toHaveBeenCalledWith(i18nSetup.Context); }); + it('passes uiCapabilities to ui/capabilities', () => { + const legacyPlatform = new LegacyPlatformService({ + ...defaultParams, + }); + + legacyPlatform.setup(defaultSetupDeps); + + expect(mockUICapabilitiesInit).toHaveBeenCalledTimes(1); + expect(mockUICapabilitiesInit).toHaveBeenCalledWith(capabilitiesSetup); + }); + it('passes fatalErrors service to ui/notify/fatal_errors', () => { const legacyPlatform = new LegacyPlatformService({ ...defaultParams, diff --git a/src/core/public/legacy/legacy_service.ts b/src/core/public/legacy/legacy_service.ts index 2095b7fb60e81b..e503b1b05013aa 100644 --- a/src/core/public/legacy/legacy_service.ts +++ b/src/core/public/legacy/legacy_service.ts @@ -44,6 +44,7 @@ export class LegacyPlatformService { notifications, http, basePath, + capabilities, uiSettings, chrome, } = core; @@ -54,6 +55,7 @@ export class LegacyPlatformService { require('ui/i18n').__newPlatformInit__(i18n.Context); require('ui/notify/fatal_error').__newPlatformInit__(fatalErrors); require('ui/notify/toasts').__newPlatformInit__(notifications.toasts); + require('ui/capabilities').__newPlatformInit__(capabilities); require('ui/chrome/api/loading_count').__newPlatformInit__(http); require('ui/chrome/api/base_path').__newPlatformInit__(basePath); require('ui/chrome/api/ui_settings').__newPlatformInit__(uiSettings); diff --git a/src/core/public/injected_metadata/deep_freeze.test.ts b/src/core/public/utils/deep_freeze.test.ts similarity index 100% rename from src/core/public/injected_metadata/deep_freeze.test.ts rename to src/core/public/utils/deep_freeze.test.ts diff --git a/src/core/public/injected_metadata/deep_freeze.ts b/src/core/public/utils/deep_freeze.ts similarity index 100% rename from src/core/public/injected_metadata/deep_freeze.ts rename to src/core/public/utils/deep_freeze.ts diff --git a/src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/index.ts b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts similarity index 100% rename from src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/index.ts rename to src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/index.ts diff --git a/src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json b/src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json similarity index 100% rename from src/core/public/injected_metadata/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json rename to src/core/public/utils/integration_tests/__fixtures__/frozen_object_mutation/tsconfig.json diff --git a/src/core/public/injected_metadata/integration_tests/deep_freeze.test.ts b/src/core/public/utils/integration_tests/deep_freeze.test.ts similarity index 100% rename from src/core/public/injected_metadata/integration_tests/deep_freeze.test.ts rename to src/core/public/utils/integration_tests/deep_freeze.test.ts diff --git a/src/es_archiver/lib/indices/kibana_index.js b/src/es_archiver/lib/indices/kibana_index.js index d0477186aee0b7..cbb3cc9f9cbda3 100644 --- a/src/es_archiver/lib/indices/kibana_index.js +++ b/src/es_archiver/lib/indices/kibana_index.js @@ -150,6 +150,7 @@ export async function createDefaultSpace({ index, client }) { space: { name: 'Default Space', description: 'This is the default space', + disabledFeatures: [], _reserved: true } } diff --git a/src/legacy/core_plugins/console/index.js b/src/legacy/core_plugins/console/index.js index 652696bc7bfd84..fa56584d9cabda 100644 --- a/src/legacy/core_plugins/console/index.js +++ b/src/legacy/core_plugins/console/index.js @@ -110,7 +110,12 @@ export default function (kibana) { defaultVars = { elasticsearchUrl: url.format( Object.assign(url.parse(head(legacyEsConfig.hosts)), { auth: false }) - ) + ), + uiCapabilities: { + dev_tools: { + show: true + }, + } }; server.route(createProxyRoute({ diff --git a/src/legacy/core_plugins/console/public/console.js b/src/legacy/core_plugins/console/public/console.js index a623927b93088b..f5ba87722e1e1e 100644 --- a/src/legacy/core_plugins/console/public/console.js +++ b/src/legacy/core_plugins/console/public/console.js @@ -23,6 +23,7 @@ import template from './index.html'; require('brace'); require('ui/autoload/styles'); +require('ui/capabilities/route_setup'); require('./src/controllers/sense_controller'); require('./src/directives/sense_history'); @@ -33,6 +34,7 @@ require('./src/directives/console_menu_directive'); uiRoutes.when('/dev_tools/console', { + requireUICapability: 'dev_tools.show', controller: 'SenseController', template, }); diff --git a/src/legacy/core_plugins/console/server/proxy_route.js b/src/legacy/core_plugins/console/server/proxy_route.js index 963f26bd3523c3..b43514b781a489 100644 --- a/src/legacy/core_plugins/console/server/proxy_route.js +++ b/src/legacy/core_plugins/console/server/proxy_route.js @@ -65,6 +65,7 @@ export const createProxyRoute = ({ path: '/api/console/proxy', method: 'POST', config: { + tags: ['access:console'], payload: { output: 'stream', parse: false diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index b2a9678fe23512..af78119c046ac6 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -138,9 +138,61 @@ export default function (kibana) { }, injectDefaultVars(server, options) { + const { savedObjects } = server; + return { kbnIndex: options.index, - kbnBaseUrl + kbnBaseUrl, + uiCapabilities: { + discover: { + show: true, + createShortUrl: true, + save: true, + }, + visualize: { + show: true, + createShortUrl: true, + delete: true, + save: true, + }, + dashboard: { + createNew: true, + show: true, + showWriteControls: true, + }, + catalogue: { + discover: true, + dashboard: true, + visualize: true, + console: true, + advanced_settings: true, + index_patterns: true, + }, + advancedSettings: { + save: true + }, + indexPatterns: { + createNew: true, + }, + savedObjectsManagement: savedObjects.types.reduce((acc, type) => ({ + ...acc, + [type]: { + delete: true, + edit: true, + read: true, + } + }), {}), + management: { + /* + * Management settings correspond to management section/link ids, and should not be changed + * without also updating those definitions. + */ + kibana: { + settings: true, + index_patterns: true, + }, + } + } }; }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js index a4fcbe27695bb7..e6ba5c828114c7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_app.js @@ -500,6 +500,7 @@ app.directive('dashboardApp', function ($injector) { showShareContextMenu({ anchorElement, allowEmbed: true, + allowShortUrl: !dashboardConfig.getHideWriteControls(), getUnhashableStates, objectId: dash.id, objectType: 'dashboard', diff --git a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js index 8267e7626fb911..7179f810db084d 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/dashboard_config.js @@ -18,9 +18,11 @@ */ import { uiModules } from 'ui/modules'; +import { uiCapabilities } from 'ui/capabilities'; + uiModules.get('kibana') .provider('dashboardConfig', () => { - let hideWriteControls = false; + let hideWriteControls = !uiCapabilities.dashboard.showWriteControls; return { /** diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.js b/src/legacy/core_plugins/kibana/public/dashboard/index.js index 000f5e840a8dcd..98c2d4f48e8948 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.js @@ -37,6 +37,7 @@ import { recentlyAccessed } from 'ui/persisted_log'; import { SavedObjectRegistryProvider } from 'ui/saved_objects/saved_object_registry'; import { DashboardListing, EMPTY_FILTER } from './listing/dashboard_listing'; import { uiModules } from 'ui/modules'; +import 'ui/capabilities/route_setup'; const app = uiModules.get('app/dashboard', [ 'ngRoute', @@ -55,7 +56,8 @@ function createNewDashboardCtrl($scope, i18n) { uiRoutes .defaults(/dashboard/, { - requireDefaultIndex: true + requireDefaultIndex: true, + requireUICapability: 'dashboard.show' }) .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, @@ -117,6 +119,7 @@ uiRoutes .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, { template: dashboardTemplate, controller: createNewDashboardCtrl, + requireUICapability: 'dashboard.createNew', resolve: { dash: function (savedDashboards, redirectWhenMissing) { return savedDashboards.get() diff --git a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap index 4d860b8843fb7f..ce81f5f2e61305 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap +++ b/src/legacy/core_plugins/kibana/public/dashboard/listing/__snapshots__/dashboard_listing.test.js.snap @@ -2,13 +2,12 @@ exports[`after fetch hideWriteControls 1`] = ` , isDisabled: ({ embeddable }) => !embeddable || !embeddable.metadata || !embeddable.metadata.editUrl, - isVisible: ({ containerState }) => containerState.viewMode === DashboardViewMode.EDIT, + isVisible: ({ containerState, embeddable }) => { + const canEditEmbeddable = Boolean( + embeddable && embeddable.metadata && embeddable.metadata.editable + ); + const inDashboardEditMode = containerState.viewMode === DashboardViewMode.EDIT; + return canEditEmbeddable && inDashboardEditMode; + }, getHref: ({ embeddable }) => { if (embeddable && embeddable.metadata.editUrl) { return embeddable.metadata.editUrl; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js index febf53de9670ad..9984153fd30afe 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.js @@ -21,6 +21,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { uiCapabilities } from 'ui/capabilities'; import { toastNotifications } from 'ui/notify'; import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder'; @@ -87,18 +88,20 @@ export class DashboardAddPanel extends React.Component { )} /> - - - - - - - - - + { uiCapabilities.visualize.save ? ( + + + + + + + + + + ) : null } ); } diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js index eccf9198939e34..8d98fc8cfe8082 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/add_panel.test.js @@ -25,6 +25,16 @@ import { DashboardAddPanel, } from './add_panel'; +jest.mock('ui/capabilities', + () => ({ + uiCapabilities: { + visualize: { + show: true, + save: true + } + } + }), { virtual: true }); + jest.mock('ui/notify', () => ({ toastNotifications: { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js index 17cb96eba7f9f6..e46cf388f5a650 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js +++ b/src/legacy/core_plugins/kibana/public/dashboard/top_nav/get_top_nav_config.js @@ -35,7 +35,8 @@ export function getTopNavConfig(dashboardMode, actions, hideWriteControls) { return ( hideWriteControls ? [ - getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]) + getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), + getShareConfig(actions[TopNavIds.SHARE]), ] : [ getFullScreenConfig(actions[TopNavIds.FULL_SCREEN]), diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js index 1f4b3103af7c16..a27f63d89cb465 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/discover_field.js @@ -24,6 +24,7 @@ import 'ui/directives/css_truncate'; import 'ui/directives/field_name'; import './string_progress_bar'; import detailsHtml from './lib/detail_views/string.html'; +import { uiCapabilities } from 'ui/capabilities'; import { uiModules } from 'ui/modules'; const app = uiModules.get('apps/discover'); @@ -76,6 +77,8 @@ app.directive('discoverField', function ($compile, i18n) { }; + $scope.canVisualize = uiCapabilities.visualize.show; + $scope.toggleDisplay = function (field) { if (field.display) { $scope.onRemoveField(field.name); diff --git a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html index 37786c3812ec53..5d134911fc91b7 100644 --- a/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html +++ b/src/legacy/core_plugins/kibana/public/discover/components/field_chooser/lib/detail_views/string.html @@ -87,7 +87,7 @@ diff --git a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js index 9b7ce8925cab39..463a7bebf14ebf 100644 --- a/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/controllers/discover.js @@ -68,6 +68,7 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal'; import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal'; import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../breadcrumbs'; import { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/build_pipeline'; +import 'ui/capabilities/route_setup'; const fetchStatuses = { UNINITIALIZED: 'uninitialized', @@ -85,12 +86,13 @@ const app = uiModules.get('apps/discover', [ uiRoutes .defaults(/^\/discover(\/|$)/, { requireDefaultIndex: true, + requireUICapability: 'discover.show', k7Breadcrumbs: ($route, $injector) => $injector.invoke( $route.current.params.id ? getSavedSearchBreadcrumbs : getRootBreadcrumbs - ) + ), }) .when('/discover/:id?', { template: indexTemplate, @@ -173,6 +175,7 @@ function discoverController( kbnUrl, localStorage, i18n, + uiCapabilities, ) { const visualizeLoader = Private(VisualizeLoaderProvider); let visualizeHandler; @@ -211,110 +214,131 @@ function discoverController( dirty: !savedSearch.id }; - $scope.topNavMenu = [{ - key: 'new', - label: i18n('kbn.discover.localMenu.localMenu.newSearchTitle', { - defaultMessage: 'New', - }), - description: i18n('kbn.discover.localMenu.newSearchDescription', { - defaultMessage: 'New Search', - }), - run: function () { kbnUrl.change('/discover'); }, - testId: 'discoverNewButton', - }, { - key: 'save', - label: i18n('kbn.discover.localMenu.saveTitle', { - defaultMessage: 'Save', - }), - description: i18n('kbn.discover.localMenu.saveSearchDescription', { - defaultMessage: 'Save Search', - }), - testId: 'discoverSaveButton', - run: async () => { - const onSave = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => { - const currentTitle = savedSearch.title; - savedSearch.title = newTitle; - savedSearch.copyOnSave = newCopyOnSave; - const saveOptions = { - confirmOverwrite: false, - isTitleDuplicateConfirmed, - onTitleDuplicate, + const getTopNavLinks = () => { + const newSearch = { + key: 'new', + label: i18n('kbn.discover.localMenu.localMenu.newSearchTitle', { + defaultMessage: 'New', + }), + description: i18n('kbn.discover.localMenu.newSearchDescription', { + defaultMessage: 'New Search', + }), + run: function () { kbnUrl.change('/discover'); }, + testId: 'discoverNewButton', + }; + + const saveSearch = { + key: 'save', + label: i18n('kbn.discover.localMenu.saveTitle', { + defaultMessage: 'Save', + }), + description: i18n('kbn.discover.localMenu.saveSearchDescription', { + defaultMessage: 'Save Search', + }), + testId: 'discoverSaveButton', + run: async () => { + const onSave = ({ newTitle, newCopyOnSave, isTitleDuplicateConfirmed, onTitleDuplicate }) => { + const currentTitle = savedSearch.title; + savedSearch.title = newTitle; + savedSearch.copyOnSave = newCopyOnSave; + const saveOptions = { + confirmOverwrite: false, + isTitleDuplicateConfirmed, + onTitleDuplicate, + }; + return saveDataSource(saveOptions).then(({ id, error }) => { + // If the save wasn't successful, put the original values back. + if (!id || error) { + savedSearch.title = currentTitle; + } + return { id, error }; + }); }; - return saveDataSource(saveOptions).then(({ id, error }) => { - // If the save wasn't successful, put the original values back. - if (!id || error) { - savedSearch.title = currentTitle; + + const saveModal = ( + { }} + title={savedSearch.title} + showCopyOnSave={savedSearch.id ? true : false} + objectType="search" + />); + showSaveModal(saveModal); + } + }; + + const openSearch = { + key: 'open', + label: i18n('kbn.discover.localMenu.openTitle', { + defaultMessage: 'Open', + }), + description: i18n('kbn.discover.localMenu.openSavedSearchDescription', { + defaultMessage: 'Open Saved Search', + }), + testId: 'discoverOpenButton', + run: () => { + showOpenSearchPanel({ + makeUrl: (searchId) => { + return kbnUrl.eval('#/discover/{{id}}', { id: searchId }); } - return { id, error }; }); - }; + } + }; - const saveModal = ( - {}} - title={savedSearch.title} - showCopyOnSave={savedSearch.id ? true : false} - objectType="search" - />); - showSaveModal(saveModal); - } - }, { - key: 'open', - label: i18n('kbn.discover.localMenu.openTitle', { - defaultMessage: 'Open', - }), - description: i18n('kbn.discover.localMenu.openSavedSearchDescription', { - defaultMessage: 'Open Saved Search', - }), - testId: 'discoverOpenButton', - run: () => { - showOpenSearchPanel({ - makeUrl: (searchId) => { - return kbnUrl.eval('#/discover/{{id}}', { id: searchId }); - } - }); - } - }, { - key: 'share', - label: i18n('kbn.discover.localMenu.shareTitle', { - defaultMessage: 'Share', - }), - description: i18n('kbn.discover.localMenu.shareSearchDescription', { - defaultMessage: 'Share Search', - }), - testId: 'shareTopNavButton', - run: async (menuItem, navController, anchorElement) => { - const sharingData = await this.getSharingData(); - showShareContextMenu({ - anchorElement, - allowEmbed: false, - getUnhashableStates, - objectId: savedSearch.id, - objectType: 'search', - shareContextMenuExtensions, - sharingData: { - ...sharingData, - title: savedSearch.title, - }, - isDirty: $appStatus.dirty, - }); - } - }, { - key: 'inspect', - label: i18n('kbn.discover.localMenu.inspectTitle', { - defaultMessage: 'Inspect', - }), - description: i18n('kbn.discover.localMenu.openInspectorForSearchDescription', { - defaultMessage: 'Open Inspector for search', - }), - testId: 'openInspectorButton', - run() { - Inspector.open(inspectorAdapters, { - title: savedSearch.title - }); - } - }]; + const shareSearch = { + key: 'share', + label: i18n('kbn.discover.localMenu.shareTitle', { + defaultMessage: 'Share', + }), + description: i18n('kbn.discover.localMenu.shareSearchDescription', { + defaultMessage: 'Share Search', + }), + testId: 'shareTopNavButton', + run: async (menuItem, navController, anchorElement) => { + const sharingData = await this.getSharingData(); + showShareContextMenu({ + anchorElement, + allowEmbed: false, + allowShortUrl: uiCapabilities.discover.createShortUrl, + getUnhashableStates, + objectId: savedSearch.id, + objectType: 'search', + shareContextMenuExtensions, + sharingData: { + ...sharingData, + title: savedSearch.title, + }, + isDirty: $appStatus.dirty, + }); + } + }; + + const inspectSearch = { + key: 'inspect', + label: i18n('kbn.discover.localMenu.inspectTitle', { + defaultMessage: 'Inspect', + }), + description: i18n('kbn.discover.localMenu.openInspectorForSearchDescription', { + defaultMessage: 'Open Inspector for search', + }), + testId: 'openInspectorButton', + run() { + Inspector.open(inspectorAdapters, { + title: savedSearch.title + }); + } + }; + + return [ + newSearch, + ...uiCapabilities.discover.save ? [saveSearch] : [], + openSearch, + shareSearch, + inspectSearch, + ]; + }; + + $scope.topNavMenu = getTopNavLinks(); // the actual courier.SearchSource $scope.searchSource = savedSearch.searchSource; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts index 28610ef01bd31b..079fb1f72bf9fb 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts @@ -58,6 +58,7 @@ interface SearchEmbeddableConfig { onEmbeddableStateChanged: OnEmbeddableStateChanged; savedSearch: SavedSearch; editUrl: string; + editable: boolean; $rootScope: ng.IRootScopeService; $compile: ng.ICompileService; } @@ -80,6 +81,7 @@ export class SearchEmbeddable extends Embeddable { constructor({ onEmbeddableStateChanged, savedSearch, + editable, editUrl, $rootScope, $compile, @@ -87,6 +89,7 @@ export class SearchEmbeddable extends Embeddable { super({ title: savedSearch.title, editUrl, + editable, indexPatterns: _.compact([savedSearch.searchSource.getField('index')]), }); this.onEmbeddableStateChanged = onEmbeddableStateChanged; diff --git a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts index d869643f2f928f..fbfa930fe145e0 100644 --- a/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts +++ b/src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable_factory.ts @@ -18,7 +18,7 @@ */ import '../doc_table'; - +import { uiCapabilities } from 'ui/capabilities'; import { i18n } from '@kbn/i18n'; import { EmbeddableFactory } from 'ui/embeddable'; import { @@ -63,6 +63,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory { onEmbeddableStateChanged: OnEmbeddableStateChanged ) { const editUrl = this.getEditPath(id); + const editable = uiCapabilities.discover.save as boolean; // can't change this to be async / awayt, because an Anglular promise is expected to be returned. return this.searchLoader.get(id).then(savedObject => { @@ -70,6 +71,7 @@ export class SearchEmbeddableFactory extends EmbeddableFactory { onEmbeddableStateChanged, savedSearch: savedObject, editUrl, + editable, $rootScope: this.$rootScope, $compile: this.$compile, }); diff --git a/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html b/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html index 2a046a0549cb57..645855766fab8e 100644 --- a/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html +++ b/src/legacy/core_plugins/kibana/public/home/home_ng_wrapper.html @@ -1,4 +1,5 @@ diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 68998f78a24573..1bc91360eddb14 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -65,6 +65,7 @@ import 'leaflet'; routes.enable(); + routes .otherwise({ redirectTo: `/${chrome.getInjected('kbnDefaultAppId', 'discover')}` diff --git a/src/legacy/core_plugins/kibana/public/management/app.html b/src/legacy/core_plugins/kibana/public/management/app.html index 2a3ecd62a895ca..11198c02960c70 100644 --- a/src/legacy/core_plugins/kibana/public/management/app.html +++ b/src/legacy/core_plugins/kibana/public/management/app.html @@ -1,4 +1,4 @@

\ No newline at end of file + diff --git a/src/legacy/core_plugins/kibana/public/management/index.js b/src/legacy/core_plugins/kibana/public/management/index.js index 3001e0c0cc066b..4f015ee28bf335 100644 --- a/src/legacy/core_plugins/kibana/public/management/index.js +++ b/src/legacy/core_plugins/kibana/public/management/index.js @@ -141,7 +141,7 @@ uiModules link: function ($scope) { timefilter.disableAutoRefreshSelector(); timefilter.disableTimeRangeSelector(); - $scope.sections = management.items.inOrder; + $scope.sections = management.visibleItems; $scope.section = management.getSection($scope.sectionName) || management; if ($scope.section) { @@ -152,7 +152,7 @@ uiModules updateSidebar($scope.sections, $scope.section.id); $scope.$on('$destroy', () => destroyReact(SIDENAV_ID)); - management.addListener(() => updateSidebar(management.items.inOrder, $scope.section.id)); + management.addListener(() => updateSidebar(management.visibleItems, $scope.section.id)); updateLandingPage($scope.$root.chrome.getKibanaVersion()); $scope.$on('$destroy', () => destroyReact(LANDING_ID)); @@ -166,7 +166,7 @@ uiModules return { restrict: 'E', link: function ($scope) { - $scope.sections = management.items.inOrder; + $scope.sections = management.visibleItems; $scope.kbnVersion = kbnVersion; } }; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx index 9dd6588f459789..928212ba48bb72 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/create_button/create_button.tsx @@ -33,6 +33,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; interface State { isPopoverOpen: boolean; @@ -46,21 +48,26 @@ interface Props { isBeta?: boolean; onClick: () => void; }>; + uiCapabilities: UICapabilities; } -export class CreateButton extends Component { +class CreateButtonComponent extends Component { public state = { isPopoverOpen: false, }; public render() { - const { options, children } = this.props; + const { options, children, uiCapabilities } = this.props; const { isPopoverOpen } = this.state; if (!options || !options.length) { return null; } + if (!uiCapabilities.indexPatterns.createNew) { + return null; + } + if (options.length === 1) { return ( { ); }; } + +export const CreateButton = injectUICapabilities(CreateButtonComponent); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js index d071af0f15bc7a..321599100a6c8f 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index.js @@ -31,6 +31,7 @@ import { SavedObjectsClientProvider } from 'ui/saved_objects'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; import { i18n } from '@kbn/i18n'; import { I18nContext } from 'ui/i18n'; +import { UICapabilitiesProvider } from 'ui/capabilities/react'; import { EuiBadge } from '@elastic/eui'; import { getListBreadcrumbs } from './breadcrumbs'; @@ -51,11 +52,13 @@ export function updateIndexPatternList( render( - + + + , node, ); @@ -81,7 +84,8 @@ const indexPatternsResolutions = { // add a dependency to all of the subsection routes uiRoutes .defaults(/management\/kibana\/(index_patterns|index_pattern)/, { - resolve: indexPatternsResolutions + resolve: indexPatternsResolutions, + requireUICapability: 'management.kibana.index_patterns', }); uiRoutes diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx index 15e0f9a2263933..79ff3d8c624d06 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/index_pattern_table/index_pattern_table.tsx @@ -85,7 +85,7 @@ export class IndexPatternTable extends React.Component { public render() { return ( - + {this.state.showFlyout && ( this.setState({ showFlyout: false })} /> )} diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index fae371ed36a3be..230e4ada74179c 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -27,7 +27,7 @@ import { uiModules } from 'ui/modules'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ObjectsTable } from './components/objects_table'; -import { getInAppUrl } from './lib/get_in_app_url'; +import { canViewInApp, getInAppUrl } from './lib/in_app_url'; import { I18nContext } from 'ui/i18n'; import { getIndexBreadcrumbs } from './breadcrumbs'; @@ -40,6 +40,7 @@ function updateObjectsTable($scope, $injector, i18n) { const $http = $injector.get('$http'); const kbnUrl = $injector.get('kbnUrl'); const config = $injector.get('config'); + const uiCapabilites = chrome.getInjected('uiCapabilities'); const savedObjectsClient = Private(SavedObjectsClientProvider); const services = savedObjectManagementRegistry.all().map(obj => $injector.get(obj.service)); @@ -64,6 +65,7 @@ function updateObjectsTable($scope, $injector, i18n) { perPageConfig={config.get('savedObjects:perPage')} basePath={chrome.getBasePath()} newIndexPatternUrl={kbnUrl.eval('#/management/kibana/index_pattern')} + uiCapabilities={uiCapabilites} getEditUrl={(id, type) => { if (type === 'index-pattern' || type === 'indexPatterns') { return kbnUrl.eval(`#/management/kibana/index_patterns/${id}`); @@ -79,6 +81,9 @@ function updateObjectsTable($scope, $injector, i18n) { return kbnUrl.eval(`#/management/kibana/objects/${serviceName}/${id}`); }} + canGoInApp={(type) => { + return canViewInApp(uiCapabilites, type); + }} goInApp={(id, type) => { kbnUrl.change(getInAppUrl(id, type)); $scope.$apply(); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html index 5922160dae85ef..6efef7b48fa0e8 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html @@ -1,4 +1,4 @@ - +
@@ -8,6 +8,15 @@ i18n-id="kbn.management.objects.view.editItemTitle" i18n-default-message="Edit {title}" i18n-values="{ title }" + ng-if="canEdit" + > + +

@@ -15,6 +24,7 @@
@@ -29,6 +39,8 @@ diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js index d784afe504f128..9bf6bcd436d9b0 100644 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js +++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec.js @@ -62,6 +62,7 @@ export class PluginSpec { deprecations, preInit, init, + postInit, isEnabled, } = options; @@ -81,6 +82,7 @@ export class PluginSpec { this._isEnabled = isEnabled; this._preInit = preInit; this._init = init; + this._postInit = postInit; if (!this.getId()) { throw createInvalidPluginError(this, 'Unable to determine plugin id'); @@ -176,6 +178,10 @@ export class PluginSpec { return this._init; } + getPostInitHandler() { + return this._postInit; + } + getConfigPrefix() { return this._configPrefix || this.getId(); } diff --git a/src/legacy/server/plugins/initialize_mixin.js b/src/legacy/server/plugins/initialize_mixin.js index 2fb08e83efd00b..9cc317f002c5af 100644 --- a/src/legacy/server/plugins/initialize_mixin.js +++ b/src/legacy/server/plugins/initialize_mixin.js @@ -43,4 +43,5 @@ export async function initializeMixin(kbnServer, server, config) { await callHookOnPlugins('preInit'); await callHookOnPlugins('init'); + await callHookOnPlugins('postInit'); } diff --git a/src/legacy/server/plugins/lib/plugin.js b/src/legacy/server/plugins/lib/plugin.js index 46dd047a657cc1..98bbbca35ececd 100644 --- a/src/legacy/server/plugins/lib/plugin.js +++ b/src/legacy/server/plugins/lib/plugin.js @@ -43,12 +43,14 @@ export class Plugin { this.requiredIds = spec.getRequiredPluginIds() || []; this.externalPreInit = spec.getPreInitHandler(); this.externalInit = spec.getInitHandler(); + this.externalPostInit = spec.getPostInitHandler(); this.enabled = spec.isEnabled(kbnServer.config); this.configPrefix = spec.getConfigPrefix(); this.publicDir = spec.getPublicDir(); this.preInit = once(this.preInit); this.init = once(this.init); + this.postInit = once(this.postInit); } async preInit() { @@ -96,6 +98,12 @@ export class Plugin { } } + async postInit() { + if (this.externalPostInit) { + return await this.externalPostInit(this.kbnServer.server); + } + } + getServer() { return this._server; } diff --git a/src/legacy/ui/public/capabilities/index.ts b/src/legacy/ui/public/capabilities/index.ts new file mode 100644 index 00000000000000..ca798d2d70ea2a --- /dev/null +++ b/src/legacy/ui/public/capabilities/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Capabilities as UICapabilities, CapabilitiesSetup } from '../../../../core/public'; + +export { Capabilities as UICapabilities } from '../../../../core/public'; +export let uiCapabilities: UICapabilities = null!; + +export function __newPlatformInit__(capabililitiesService: CapabilitiesSetup) { + if (uiCapabilities) { + throw new Error('ui/capabilities already initialized with new platform apis'); + } + + uiCapabilities = capabililitiesService.getCapabilities(); +} diff --git a/src/legacy/ui/public/capabilities/react/index.ts b/src/legacy/ui/public/capabilities/react/index.ts new file mode 100644 index 00000000000000..78d95a2603290a --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { UICapabilitiesProvider } from './ui_capabilities_provider'; +export { injectUICapabilities } from './inject_ui_capabilities'; diff --git a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx new file mode 100644 index 00000000000000..f89112fd824292 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.test.tsx @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + uiCapability1: true, + uiCapability2: { + nestedProp: 'nestedValue', + }, + }, +})); + +import { mount } from 'enzyme'; +import React from 'react'; +import { UICapabilities } from '..'; +import { injectUICapabilities } from './inject_ui_capabilities'; +import { UICapabilitiesProvider } from './ui_capabilities_provider'; + +describe('injectUICapabilities', () => { + it('provides UICapabilities to SFCs', () => { + interface SFCProps { + uiCapabilities: UICapabilities; + } + + const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => { + return {uiCapabilities.uiCapability2.nestedProp}; + }); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); + + it('provides UICapabilities to class components', () => { + interface ClassProps { + uiCapabilities: UICapabilities; + } + + class MyClassComponent extends React.Component { + public render() { + return {this.props.uiCapabilities.uiCapability2.nestedProp}; + } + } + + const WrappedComponent = injectUICapabilities(MyClassComponent); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); +}); diff --git a/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx new file mode 100644 index 00000000000000..5f12c20119907d --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/inject_ui_capabilities.tsx @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { Component, ComponentClass, ComponentType } from 'react'; +import { UICapabilities } from '..'; +import { UICapabilitiesContext } from './ui_capabilities_context'; + +function getDisplayName(component: ComponentType) { + return component.displayName || component.name || 'Component'; +} + +interface InjectedProps { + uiCapabilities: UICapabilities; +} + +export function injectUICapabilities

( + WrappedComponent: ComponentType

+): ComponentClass>> & { + WrappedComponent: ComponentType

; +} { + class InjectUICapabilities extends Component { + public static displayName = `InjectUICapabilities(${getDisplayName(WrappedComponent)})`; + + public static WrappedComponent: ComponentType

= WrappedComponent; + + public static contextType = UICapabilitiesContext; + + constructor(props: any, context: any) { + super(props, context); + } + + public render() { + return ; + } + } + return InjectUICapabilities; +} diff --git a/src/legacy/ui/public/capabilities/react/legacy/index.ts b/src/legacy/ui/public/capabilities/react/legacy/index.ts new file mode 100644 index 00000000000000..78d95a2603290a --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { UICapabilitiesProvider } from './ui_capabilities_provider'; +export { injectUICapabilities } from './inject_ui_capabilities'; diff --git a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx new file mode 100644 index 00000000000000..1ee8ba72c6ccb0 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.test.tsx @@ -0,0 +1,113 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + uiCapability1: true, + uiCapability2: { + nestedProp: 'nestedValue', + }, + }, +})); + +import { mount } from 'enzyme'; +import React from 'react'; +import { UICapabilities } from '../..'; +import { injectUICapabilities } from './inject_ui_capabilities'; +import { UICapabilitiesProvider } from './ui_capabilities_provider'; + +describe('injectUICapabilities', () => { + it('provides UICapabilities to SFCs', () => { + interface SFCProps { + uiCapabilities: UICapabilities; + } + + const MySFC = injectUICapabilities(({ uiCapabilities }: SFCProps) => { + return {uiCapabilities.uiCapability2.nestedProp}; + }); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); + + it('provides UICapabilities to class components', () => { + interface ClassProps { + uiCapabilities: UICapabilities; + } + + class MyClassComponent extends React.Component { + public render() { + return {this.props.uiCapabilities.uiCapability2.nestedProp}; + } + } + + const WrappedComponent = injectUICapabilities(MyClassComponent); + + const wrapper = mount( + + + + ); + + expect(wrapper).toMatchInlineSnapshot(` + + + + + nestedValue + + + + +`); + }); +}); diff --git a/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx new file mode 100644 index 00000000000000..0253326809721c --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/inject_ui_capabilities.tsx @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import PropTypes from 'prop-types'; +import React, { Component, ComponentClass, ComponentType } from 'react'; +import { UICapabilities } from '../..'; + +function getDisplayName(component: ComponentType) { + return component.displayName || component.name || 'Component'; +} + +interface InjectedProps { + uiCapabilities: UICapabilities; +} + +export function injectUICapabilities

( + WrappedComponent: ComponentType

+): ComponentClass>> & { + WrappedComponent: ComponentType

; +} { + class InjectUICapabilities extends Component { + public static displayName = `InjectUICapabilities(${getDisplayName(WrappedComponent)})`; + + public static WrappedComponent: ComponentType

= WrappedComponent; + + public static contextTypes = { + uiCapabilities: PropTypes.object.isRequired, + }; + + constructor(props: any, context: any) { + super(props, context); + } + + public render() { + return ( + + ); + } + } + return InjectUICapabilities; +} diff --git a/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx new file mode 100644 index 00000000000000..856665acd65b87 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/legacy/ui_capabilities_provider.tsx @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; +import { uiCapabilities, UICapabilities } from '../..'; + +interface Props { + children: ReactNode; +} + +interface ProviderContext { + uiCapabilities: UICapabilities; +} + +export class UICapabilitiesProvider extends React.Component { + public static displayName: string = 'UICapabilitiesProvider'; + + public static childContextTypes = { + uiCapabilities: PropTypes.object.isRequired, + }; + + public getChildContext(): ProviderContext { + return { + uiCapabilities, + }; + } + + public render() { + return React.Children.only(this.props.children); + } +} diff --git a/src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts b/src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts new file mode 100644 index 00000000000000..ed299662899bb5 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/ui_capabilities_context.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { UICapabilities } from '..'; + +export const UICapabilitiesContext = React.createContext({ + navLinks: {}, + catalogue: {}, + management: {}, +}); diff --git a/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx new file mode 100644 index 00000000000000..42c10004b766b5 --- /dev/null +++ b/src/legacy/ui/public/capabilities/react/ui_capabilities_provider.tsx @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { ReactNode } from 'react'; +import { uiCapabilities } from '..'; +import { UICapabilitiesContext } from './ui_capabilities_context'; + +interface Props { + children: ReactNode; +} + +export class UICapabilitiesProvider extends React.Component { + public static displayName: string = 'UICapabilitiesProvider'; + + public render() { + return ( + + {this.props.children} + + ); + } +} diff --git a/src/legacy/ui/public/capabilities/route_setup.ts b/src/legacy/ui/public/capabilities/route_setup.ts new file mode 100644 index 00000000000000..c7817b8cc5748c --- /dev/null +++ b/src/legacy/ui/public/capabilities/route_setup.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { get } from 'lodash'; +import chrome from 'ui/chrome'; +import uiRoutes from 'ui/routes'; +import { UICapabilities } from '.'; + +uiRoutes.addSetupWork( + (uiCapabilities: UICapabilities, kbnBaseUrl: string, $route: any, kbnUrl: any) => { + const route = get($route, 'current.$$route') as any; + if (!route.requireUICapability) { + return; + } + + if (!get(uiCapabilities, route.requireUICapability)) { + const url = chrome.addBasePath(`${kbnBaseUrl}#/home`); + kbnUrl.redirect(url); + throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN; + } + } +); diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx index 98e176fdc704f0..41d50e7b8be286 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/components/header.tsx @@ -52,6 +52,7 @@ import { import { i18n } from '@kbn/i18n'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; +import { UICapabilities } from 'ui/capabilities'; import chrome, { NavLink } from 'ui/chrome'; import { HelpExtension } from 'ui/chrome'; import { RecentlyAccessedHistoryItem } from 'ui/persisted_log'; @@ -76,6 +77,7 @@ interface Props { helpExtension$: Rx.Observable; navControls: ChromeHeaderNavControlsRegistry; intl: InjectedIntl; + uiCapabilities: UICapabilities; } // Providing a buffer between the limit and the cut off index @@ -212,7 +214,15 @@ class HeaderUI extends Component { } public render() { - const { appTitle, breadcrumbs$, isVisible, navControls, helpExtension$, intl } = this.props; + const { + appTitle, + breadcrumbs$, + isVisible, + navControls, + helpExtension$, + intl, + uiCapabilities, + } = this.props; const { navLinks, recentlyAccessed } = this.state; if (!isVisible) { @@ -223,7 +233,7 @@ class HeaderUI extends Component { const rightNavControls = navControls.bySide[NavControlSide.Right]; let navLinksArray = navLinks.map(navLink => - navLink.hidden + navLink.hidden || !uiCapabilities.navLinks[navLink.id] ? null : { key: navLink.id, diff --git a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js index 66b34b38df6c63..062ebc258b484f 100644 --- a/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js +++ b/src/legacy/ui/public/chrome/directives/header_global_nav/header_global_nav.js @@ -25,7 +25,7 @@ import { chromeHeaderNavControlsRegistry } from 'ui/registry/chrome_header_nav_c const module = uiModules.get('kibana'); -module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { +module.directive('headerGlobalNav', (reactDirective, chrome, Private, uiCapabilities) => { const { recentlyAccessed } = require('ui/persisted_log'); const navControls = Private(chromeHeaderNavControlsRegistry); const homeHref = chrome.addBasePath('/app/kibana#/home'); @@ -44,6 +44,7 @@ module.directive('headerGlobalNav', (reactDirective, chrome, Private) => { recentlyAccessed$: recentlyAccessed.get$(), forceAppSwitcherNavigation$: chrome.getForceAppSwitcherNavigation$(), navControls, - homeHref + homeHref, + uiCapabilities, }); }); diff --git a/src/legacy/ui/public/embeddable/embeddable.ts b/src/legacy/ui/public/embeddable/embeddable.ts index a6ce01cb2c05b4..f70ee5a114bbe1 100644 --- a/src/legacy/ui/public/embeddable/embeddable.ts +++ b/src/legacy/ui/public/embeddable/embeddable.ts @@ -39,6 +39,11 @@ export interface EmbeddableMetadata { * offer for editing directly on the dashboard. */ editUrl?: string; + + /** + * A flag indicating if this embeddable can be edited. + */ + editable?: boolean; } export abstract class Embeddable { diff --git a/src/legacy/ui/public/management/components/sidebar_nav.test.ts b/src/legacy/ui/public/management/components/sidebar_nav.test.ts index 181c3c76a6c119..e02cc7d2901b64 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.test.ts +++ b/src/legacy/ui/public/management/components/sidebar_nav.test.ts @@ -36,25 +36,25 @@ const visibleItem = { display: 'item', id: 'item', ...activeProps }; const notVisibleSection = { display: 'Not visible', id: 'not-visible', - items: toIndexedArray([visibleItem]), + visibleItems: toIndexedArray([visibleItem]), ...notVisibleProps, }; const disabledSection = { display: 'Disabled', id: 'disabled', - items: toIndexedArray([visibleItem]), + visibleItems: toIndexedArray([visibleItem]), ...disabledProps, }; const noItemsSection = { display: 'No items', id: 'no-items', - items: toIndexedArray([]), + visibleItems: toIndexedArray([]), ...activeProps, }; const noActiveItemsSection = { display: 'No active items', id: 'no-active-items', - items: toIndexedArray([ + visibleItems: toIndexedArray([ { display: 'disabled', id: 'disabled', ...disabledProps }, { display: 'notVisible', id: 'notVisible', ...notVisibleProps }, ]), @@ -63,7 +63,7 @@ const noActiveItemsSection = { const activeSection = { display: 'activeSection', id: 'activeSection', - items: toIndexedArray([visibleItem]), + visibleItems: toIndexedArray([visibleItem]), ...activeProps, }; diff --git a/src/legacy/ui/public/management/components/sidebar_nav.tsx b/src/legacy/ui/public/management/components/sidebar_nav.tsx index 373fdadc2ae421..ef232c7ef7edac 100644 --- a/src/legacy/ui/public/management/components/sidebar_nav.tsx +++ b/src/legacy/ui/public/management/components/sidebar_nav.tsx @@ -31,7 +31,7 @@ interface Subsection { icon?: IconType; } interface Section extends Subsection { - items: IndexedArray; + visibleItems: IndexedArray; } const sectionVisible = (section: Subsection) => !section.disabled && section.visible; @@ -47,9 +47,9 @@ const sectionToNav = (selectedId: string) => ({ display, id, url, icon }: Subsec export const sideNavItems = (sections: Section[], selectedId: string) => sections .filter(sectionVisible) - .filter(section => section.items.filter(sectionVisible).length) + .filter(section => section.visibleItems.filter(sectionVisible).length) .map(section => ({ - items: section.items.inOrder.filter(sectionVisible).map(sectionToNav(selectedId)), + items: section.visibleItems.filter(sectionVisible).map(sectionToNav(selectedId)), ...sectionToNav(selectedId)(section), })); diff --git a/src/legacy/ui/public/management/section.js b/src/legacy/ui/public/management/section.js index 72b44d978fe31c..76fe4f024f3958 100644 --- a/src/legacy/ui/public/management/section.js +++ b/src/legacy/ui/public/management/section.js @@ -19,6 +19,7 @@ import { assign } from 'lodash'; import { IndexedArray } from '../indexed_array'; +import { uiCapabilities } from '../capabilities'; const listeners = []; @@ -50,10 +51,16 @@ export class ManagementSection { this.url = ''; assign(this, options); + } get visibleItems() { - return this.items.inOrder.filter(item => item.visible); + return this.items.inOrder.filter(item => { + const capabilityManagementSection = uiCapabilities.management[this.id]; + const itemCapability = capabilityManagementSection ? capabilityManagementSection[item.id] : null; + + return item.visible && itemCapability !== false; + }); } /** diff --git a/src/legacy/ui/public/management/__tests__/section.js b/src/legacy/ui/public/management/section.test.js similarity index 70% rename from src/legacy/ui/public/management/__tests__/section.js rename to src/legacy/ui/public/management/section.test.js index cc49d346213d63..eff92fb3078943 100644 --- a/src/legacy/ui/public/management/__tests__/section.js +++ b/src/legacy/ui/public/management/section.test.js @@ -16,53 +16,62 @@ * specific language governing permissions and limitations * under the License. */ +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + navLinks: {}, + management: { + kibana: { + sampleFeature1: true, + sampleFeature2: false, + } + } + } +})); -import expect from '@kbn/expect'; - -import { ManagementSection } from '../section'; -import { IndexedArray } from '../../indexed_array'; +import { ManagementSection } from './section'; +import { IndexedArray } from '../indexed_array'; describe('ManagementSection', () => { describe('constructor', () => { it('defaults display to id', () => { const section = new ManagementSection('kibana'); - expect(section.display).to.be('kibana'); + expect(section.display).toBe('kibana'); }); it('defaults visible to true', () => { const section = new ManagementSection('kibana'); - expect(section.visible).to.be(true); + expect(section.visible).toBe(true); }); it('defaults disabled to false', () => { const section = new ManagementSection('kibana'); - expect(section.disabled).to.be(false); + expect(section.disabled).toBe(false); }); it('defaults tooltip to empty string', () => { const section = new ManagementSection('kibana'); - expect(section.tooltip).to.be(''); + expect(section.tooltip).toBe(''); }); it('defaults url to empty string', () => { const section = new ManagementSection('kibana'); - expect(section.url).to.be(''); + expect(section.url).toBe(''); }); it('exposes items', () => { const section = new ManagementSection('kibana'); - expect(section.items).to.be.empty(); + expect(section.items).toHaveLength(0); }); it('exposes visibleItems', () => { const section = new ManagementSection('kibana'); - expect(section.visibleItems).to.be.empty(); + expect(section.visibleItems).toHaveLength(0); }); it('assigns all options', () => { const section = new ManagementSection('kibana', { description: 'test', url: 'foobar' }); - expect(section.description).to.be('test'); - expect(section.url).to.be('foobar'); + expect(section.description).toBe('test'); + expect(section.url).toBe('foobar'); }); }); @@ -74,19 +83,19 @@ describe('ManagementSection', () => { }); it('returns a ManagementSection', () => { - expect(section.register('about')).to.be.a(ManagementSection); + expect(section.register('about')).toBeInstanceOf(ManagementSection); }); it('provides a reference to the parent', () => { - expect(section.register('about').parent).to.be(section); + expect(section.register('about').parent).toBe(section); }); it('adds item', function () { section.register('about', { description: 'test' }); - expect(section.items).to.have.length(1); - expect(section.items[0]).to.be.a(ManagementSection); - expect(section.items[0].id).to.be('about'); + expect(section.items).toHaveLength(1); + expect(section.items[0]).toBeInstanceOf(ManagementSection); + expect(section.items[0].id).toBe('about'); }); it('can only register a section once', () => { @@ -99,7 +108,7 @@ describe('ManagementSection', () => { threwException = e.message.indexOf('is already registered') > -1; } - expect(threwException).to.be(true); + expect(threwException).toBe(true); }); it('calls listener when item added', () => { @@ -108,7 +117,7 @@ describe('ManagementSection', () => { section.addListener(listenerFn); section.register('about'); - expect(listerCalled).to.be(true); + expect(listerCalled).toBe(true); }); }); @@ -122,13 +131,13 @@ describe('ManagementSection', () => { it ('deregisters an existing section', () => { section.deregister('about'); - expect(section.items).to.have.length(0); + expect(section.items).toHaveLength(0); }); it ('allows deregistering a section more than once', () => { section.deregister('about'); section.deregister('about'); - expect(section.items).to.have.length(0); + expect(section.items).toHaveLength(0); }); it('calls listener when item added', () => { @@ -137,7 +146,7 @@ describe('ManagementSection', () => { section.addListener(listenerFn); section.deregister('about'); - expect(listerCalled).to.be(true); + expect(listerCalled).toBe(true); }); }); @@ -150,21 +159,21 @@ describe('ManagementSection', () => { }); it('returns registered section', () => { - expect(section.getSection('about')).to.be.a(ManagementSection); + expect(section.getSection('about')).toBeInstanceOf(ManagementSection); }); it('returns undefined if un-registered', () => { - expect(section.getSection('unknown')).to.be(undefined); + expect(section.getSection('unknown')).not.toBeDefined(); }); it('returns sub-sections specified via a /-separated path', () => { section.getSection('about').register('time'); - expect(section.getSection('about/time')).to.be.a(ManagementSection); - expect(section.getSection('about/time')).to.be(section.getSection('about').getSection('time')); + expect(section.getSection('about/time')).toBeInstanceOf(ManagementSection); + expect(section.getSection('about/time')).toBe(section.getSection('about').getSection('time')); }); it('returns undefined if a sub-section along a /-separated path does not exist', () => { - expect(section.getSection('about/damn/time')).to.be(undefined); + expect(section.getSection('about/damn/time')).toBe(undefined); }); }); @@ -180,19 +189,19 @@ describe('ManagementSection', () => { }); it('is an indexed array', () => { - expect(section.items).to.be.a(IndexedArray); + expect(section.items).toBeInstanceOf(IndexedArray); }); it('is indexed on id', () => { const keys = Object.keys(section.items.byId).sort(); - expect(section.items.byId).to.be.an('object'); + expect(section.items.byId).toBeInstanceOf(Object); - expect(keys).to.eql(['one', 'three', 'two']); + expect(keys).toEqual(['one', 'three', 'two']); }); it('can be ordered', () => { const ids = section.items.inOrder.map((i) => { return i.id; }); - expect(ids).to.eql(['one', 'two', 'three']); + expect(ids).toEqual(['one', 'two', 'three']); }); }); @@ -205,13 +214,13 @@ describe('ManagementSection', () => { it('hide sets visible to false', () => { section.hide(); - expect(section.visible).to.be(false); + expect(section.visible).toBe(false); }); it('show sets visible to true', () => { section.hide(); section.show(); - expect(section.visible).to.be(true); + expect(section.visible).toBe(true); }); }); @@ -224,12 +233,12 @@ describe('ManagementSection', () => { it('disable sets disabled to true', () => { section.disable(); - expect(section.disabled).to.be(true); + expect(section.disabled).toBe(true); }); it('enable sets disabled to false', () => { section.enable(); - expect(section.disabled).to.be(false); + expect(section.disabled).toBe(false); }); }); @@ -246,15 +255,20 @@ describe('ManagementSection', () => { it('maintains the order', () => { const ids = section.visibleItems.map((i) => { return i.id; }); - expect(ids).to.eql(['one', 'two', 'three']); + expect(ids).toEqual(['one', 'two', 'three']); }); it('does not include hidden items', () => { section.getSection('two').hide(); const ids = section.visibleItems.map((i) => { return i.id; }); - expect(ids).to.eql(['one', 'three']); + expect(ids).toEqual(['one', 'three']); }); + it('does not include visible items hidden via uiCapabilities', () => { + section.register('sampleFeature2', { order: 4, visible: true }); + const ids = section.visibleItems.map((i) => { return i.id; }); + expect(ids).toEqual(['one', 'two', 'three']); + }); }); }); diff --git a/src/legacy/ui/public/registry/feature_catalogue.js b/src/legacy/ui/public/registry/feature_catalogue.js index af8a68d69a7d65..e9b8742f94b014 100644 --- a/src/legacy/ui/public/registry/feature_catalogue.js +++ b/src/legacy/ui/public/registry/feature_catalogue.js @@ -18,13 +18,17 @@ */ import { uiRegistry } from './_registry'; +import { uiCapabilities } from '../capabilities'; export const FeatureCatalogueRegistryProvider = uiRegistry({ name: 'featureCatalogue', index: ['id'], group: ['category'], order: ['title'], - filter: featureCatalogItem => Object.keys(featureCatalogItem).length > 0 + filter: featureCatalogItem => { + const isDisabledViaCapabilities = uiCapabilities.catalogue[featureCatalogItem.id] === false; + return !isDisabledViaCapabilities && Object.keys(featureCatalogItem).length > 0; + } }); export const FeatureCatalogueCategory = { diff --git a/src/legacy/ui/public/registry/feature_catalogue.test.js b/src/legacy/ui/public/registry/feature_catalogue.test.js new file mode 100644 index 00000000000000..bf97fbd93c15b8 --- /dev/null +++ b/src/legacy/ui/public/registry/feature_catalogue.test.js @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +jest.mock('ui/capabilities', () => ({ + uiCapabilities: { + navLinks: {}, + management: {}, + catalogue: { + item1: true, + item2: false, + item3: true, + }, + } +})); +import { FeatureCatalogueCategory, FeatureCatalogueRegistryProvider } from './feature_catalogue'; + +describe('FeatureCatalogueRegistryProvider', () => { + + beforeAll(() => { + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'item1', + title: 'foo', + description: 'this is foo', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }; + }); + + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'item2', + title: 'bar', + description: 'this is bar', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }; + }); + + // intentionally not listed in uiCapabilities.catalogue above + FeatureCatalogueRegistryProvider.register(() => { + return { + id: 'item4', + title: 'secret', + description: 'this is a secret', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }; + }); + }); + + it('should not return items hidden by uiCapabilities', () => { + const mockPrivate = entityFn => entityFn(); + const mockInjector = () => null; + + // eslint-disable-next-line new-cap + const foo = FeatureCatalogueRegistryProvider(mockPrivate, mockInjector).inTitleOrder; + expect(foo).toEqual([{ + id: 'item1', + title: 'foo', + description: 'this is foo', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }, { + id: 'item4', + title: 'secret', + description: 'this is a secret', + icon: 'savedObjectsApp', + path: '/app/kibana#/management/kibana/objects', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }]); + }); +}); diff --git a/src/legacy/ui/public/routes/route_manager.d.ts b/src/legacy/ui/public/routes/route_manager.d.ts index 10bdf9100182ff..3471d7e954862f 100644 --- a/src/legacy/ui/public/routes/route_manager.d.ts +++ b/src/legacy/ui/public/routes/route_manager.d.ts @@ -30,9 +30,11 @@ interface RouteConfiguration { resolve?: object; template?: string; k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; + requireUICapability?: string; } interface RouteManager { + addSetupWork(cb: (...args: any[]) => void): void; when(path: string, routeConfiguration: RouteConfiguration): RouteManager; otherwise(routeConfiguration: RouteConfiguration): RouteManager; defaults(path: string | RegExp, defaults: RouteConfiguration): RouteManager; diff --git a/src/legacy/ui/public/routes/routes.d.ts b/src/legacy/ui/public/routes/routes.d.ts index 1a0a89612bf1d3..d48230e9d56f92 100644 --- a/src/legacy/ui/public/routes/routes.d.ts +++ b/src/legacy/ui/public/routes/routes.d.ts @@ -20,6 +20,7 @@ import RouteManager from 'ui/routes/route_manager'; interface DefaultRouteManager extends RouteManager { + WAIT_FOR_URL_CHANGE_TOKEN: string; enable(): void; } diff --git a/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap b/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap index 3cdc655743eda8..068a7246e71455 100644 --- a/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap +++ b/src/legacy/ui/public/share/components/__snapshots__/url_panel_content.test.js.snap @@ -110,6 +110,7 @@ exports[`render 1`] = ` /> `; + +exports[`should hide short url section when allowShortUrl is false 1`] = ` + + + } + labelType="label" + > + + + + + + + } + position="bottom" + type="questionInCircle" + /> + + , + }, + Object { + "data-test-subj": "exportAsSavedObject", + "disabled": false, + "id": "savedObject", + "label": + + + + + + } + position="bottom" + type="questionInCircle" + /> + + , + }, + ] + } + /> + + + + + +`; diff --git a/src/legacy/ui/public/share/components/share_context_menu.tsx b/src/legacy/ui/public/share/components/share_context_menu.tsx index b51cb5a049403c..5d5c80f10e1d5b 100644 --- a/src/legacy/ui/public/share/components/share_context_menu.tsx +++ b/src/legacy/ui/public/share/components/share_context_menu.tsx @@ -28,6 +28,7 @@ import { UrlPanelContent } from './url_panel_content'; interface Props { allowEmbed: boolean; + allowShortUrl: boolean; objectId?: string; objectType: string; getUnhashableStates: () => object[]; @@ -63,6 +64,7 @@ class ShareContextMenuUI extends Component { }), content: ( { }), content: ( { const component = shallowWithIntl( {}} />); @@ -36,6 +37,17 @@ test('render', () => { test('should enable saved object export option when objectId is provided', () => { const component = shallowWithIntl( {}} + />); + expect(component).toMatchSnapshot(); +}); + +test('should hide short url section when allowShortUrl is false', () => { + const component = shallowWithIntl( {}} diff --git a/src/legacy/ui/public/share/components/url_panel_content.tsx b/src/legacy/ui/public/share/components/url_panel_content.tsx index c579def221effb..07c7173797809c 100644 --- a/src/legacy/ui/public/share/components/url_panel_content.tsx +++ b/src/legacy/ui/public/share/components/url_panel_content.tsx @@ -42,6 +42,7 @@ import { shortenUrl } from '../lib/url_shortener'; const FixedEuiIconTip = EuiIconTip as React.SFC; interface Props { + allowShortUrl: boolean; isEmbedded?: boolean; objectId?: string; objectType: string; @@ -354,7 +355,10 @@ class UrlPanelContentUI extends Component { }; private renderShortUrlSwitch = () => { - if (this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT) { + if ( + this.state.exportUrlAs === ExportUrlAsType.EXPORT_URL_AS_SAVED_OBJECT || + !this.props.allowShortUrl + ) { return; } const shortUrlLabel = ( @@ -386,7 +390,7 @@ class UrlPanelContentUI extends Component { ); return ( - + {this.renderWithIconTip(switchComponent, tipContent)} ); diff --git a/src/legacy/ui/public/share/show_share_context_menu.tsx b/src/legacy/ui/public/share/show_share_context_menu.tsx index dcc685b0dbdf7d..1b3da0c6dc0607 100644 --- a/src/legacy/ui/public/share/show_share_context_menu.tsx +++ b/src/legacy/ui/public/share/show_share_context_menu.tsx @@ -37,6 +37,7 @@ const onClose = () => { interface ShowProps { anchorElement: any; allowEmbed: boolean; + allowShortUrl: boolean; getUnhashableStates: () => object[]; objectId?: string; objectType: string; @@ -48,6 +49,7 @@ interface ShowProps { export function showShareContextMenu({ anchorElement, allowEmbed, + allowShortUrl, getUnhashableStates, objectId, objectType, @@ -76,6 +78,7 @@ export function showShareContextMenu({ > ({ + ...acc, + [navLinkSpec._id]: true + }), {}) + } + }; + } + let defaultInjectedVars = {}; kbnServer.afterPluginsInit(() => { const { defaultInjectedVarProviders = [] } = kbnServer.uiExports; @@ -48,7 +61,7 @@ export function uiRenderMixin(kbnServer, server, config) { allDefaults, fn(kbnServer.server, pluginSpec.readConfigValue(kbnServer.config, [])) ) - ), {}); + ), getInitialDefaultInjectedVars()); }); // render all views from ./views diff --git a/test/api_integration/apis/management/saved_objects/relationships.js b/test/api_integration/apis/management/saved_objects/relationships.js index 2a866245b1aa40..d6e8030352ee13 100644 --- a/test/api_integration/apis/management/saved_objects/relationships.js +++ b/test/api_integration/apis/management/saved_objects/relationships.js @@ -43,11 +43,15 @@ export default function ({ getService }) { visualization: GENERIC_RESPONSE_SCHEMA, 'index-pattern': GENERIC_RESPONSE_SCHEMA, }); + const baseApiUrl = `/api/kibana/management/saved_objects/relationships`; + const coerceToArray = itemOrItems => [].concat(itemOrItems); + const getSavedObjectTypesQuery = types => coerceToArray(types).map(type => `savedObjectTypes=${type}`).join('&'); + const defaultQuery = getSavedObjectTypesQuery(['visualization', 'index-pattern', 'search', 'dashboard']); describe('searches', async () => { it('should validate search response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/search/960372e0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, SEARCH_RESPONSE_SCHEMA); @@ -57,7 +61,7 @@ export default function ({ getService }) { it('should work for searches', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/search/960372e0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -77,9 +81,26 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/search/960372e0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('visualization')}`) + .expect(res => console.log(res.text)) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + visualization: [ + { + id: 'a42c0580-3224-11e8-a572-ffca06da1357', + title: 'VisualizationFromSavedSearch', + }, + ] + }); + }); + }); + //TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. it.skip('should return 404 if search finds no results', async () => { - await supertest.get(`/api/kibana/management/saved_objects/relationships/search/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`).expect(404); + await supertest.get(`${baseApiUrl}/search/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx${defaultQuery}`).expect(404); }); }); @@ -90,7 +111,7 @@ export default function ({ getService }) { it('should validate dashboard response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, DASHBOARD_RESPONSE_SCHEMA); @@ -100,7 +121,7 @@ export default function ({ getService }) { it('should work for dashboards', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -118,10 +139,19 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/dashboard/b70c7ae0-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({}); + }); + }); + //TODO: https://github.com/elastic/kibana/issues/19713 causes this test to fail. it.skip('should return 404 if dashboard finds no results', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/dashboard/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) + .get(`${baseApiUrl}/dashboard/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx${defaultQuery}`) .expect(404); }); }); @@ -134,7 +164,7 @@ export default function ({ getService }) { it('should validate visualization response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/visualization/a42c0580-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, VISUALIZATIONS_RESPONSE_SCHEMA); @@ -144,7 +174,7 @@ export default function ({ getService }) { it('should work for visualizations', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/visualization/a42c0580-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -164,9 +194,25 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/visualization/a42c0580-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + search: [ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + title: 'OneRecord' + }, + ] + }); + }); + }); + it('should return 404 if visualizations finds no results', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/visualization/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) + .get(`${baseApiUrl}/visualization/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?${defaultQuery}`) .expect(404); }); }); @@ -179,7 +225,7 @@ export default function ({ getService }) { it('should validate visualization response schema', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { const validationResult = Joi.validate(resp.body, INDEX_PATTERN_RESPONSE_SCHEMA); @@ -189,7 +235,7 @@ export default function ({ getService }) { it('should work for index patterns', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357`) + .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${defaultQuery}`) .expect(200) .then(resp => { expect(resp.body).to.eql({ @@ -209,9 +255,25 @@ export default function ({ getService }) { }); }); + it('should filter based on savedObjectTypes', async () => { + await supertest + .get(`${baseApiUrl}/index-pattern/8963ca30-3224-11e8-a572-ffca06da1357?${getSavedObjectTypesQuery('search')}`) + .expect(200) + .then(resp => { + expect(resp.body).to.eql({ + search: [ + { + id: '960372e0-3224-11e8-a572-ffca06da1357', + title: 'OneRecord', + }, + ] + }); + }); + }); + it('should return 404 if index pattern finds no results', async () => { await supertest - .get(`/api/kibana/management/saved_objects/relationships/index-pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`) + .get(`${baseApiUrl}/index-pattern/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx?${defaultQuery}`) .expect(404); }); }); diff --git a/test/functional/config.js b/test/functional/config.js index 4b41ebe71c2ce5..ace10d40681a85 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -64,6 +64,9 @@ export default async function ({ readConfigFile }) { }, apps: { + kibana: { + pathname: '/app/kibana', + }, status_page: { pathname: '/status', }, diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js index 90a8689cfb5144..b16d0d04531a53 100644 --- a/test/functional/page_objects/common_page.js +++ b/test/functional/page_objects/common_page.js @@ -49,10 +49,14 @@ export function CommonPageProvider({ getService, getPageObjects }) { * @param {string} appName As defined in the apps config * @param {string} subUrl The route after the hash (#) */ - async navigateToUrl(appName, subUrl) { + async navigateToUrl(appName, subUrl, { + basePath = '', + ensureCurrentUrl = true, + shouldLoginIfPrompted = true + } = {}) { + // we onlt use the pathname from the appConfig and use the subUrl as the hash const appConfig = { - ...config.get(['apps', appName]), - // Overwrite the default hash with the URL we really want. + pathname: `${basePath}${config.get(['apps', appName]).pathname}`, hash: `${appName}/${subUrl}`, }; @@ -60,8 +64,38 @@ export function CommonPageProvider({ getService, getPageObjects }) { await retry.try(async () => { log.debug(`navigateToUrl ${appUrl}`); await browser.get(appUrl); - const currentUrl = await this.loginIfPrompted(appUrl); - if (!currentUrl.includes(appUrl)) { + + const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl) : await browser.getCurrentUrl(); + + if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { + throw new Error(`expected ${currentUrl}.includes(${appUrl})`); + } + }); + } + + /** + * @param {string} appName As defined in the apps config + * @param {string} hash The route after the hash (#) + */ + async navigateToActualUrl(appName, hash, { + basePath = '', + ensureCurrentUrl = true, + shouldLoginIfPrompted = true + } = {}) { + // we only use the apps config to get the application path + const appConfig = { + pathname: `${basePath}${config.get(['apps', appName]).pathname}`, + hash, + }; + + const appUrl = getUrl.noAuth(config.get('servers.kibana'), appConfig); + await retry.try(async () => { + log.debug(`navigateToActualUrl ${appUrl}`); + await browser.get(appUrl); + + const currentUrl = shouldLoginIfPrompted ? await this.loginIfPrompted(appUrl) : await browser.getCurrentUrl(); + + if (ensureCurrentUrl && !currentUrl.includes(appUrl)) { throw new Error(`expected ${currentUrl}.includes(${appUrl})`); } }); @@ -89,17 +123,12 @@ export function CommonPageProvider({ getService, getPageObjects }) { return currentUrl; } - - /** - * @param {string} appName - name of the app - * @param {object} [opts] - optional options object - * @param {object} [opts.appConfig] - overrides for appConfig, e.g. { pathname, hash } - */ - navigateToApp(appName, opts = { appConfig: {} }) { + navigateToApp(appName, { basePath = '', shouldLoginIfPrompted = true, hash = '' } = {}) { const self = this; + const appConfig = config.get(['apps', appName]); const appUrl = getUrl.noAuth(config.get('servers.kibana'), { - ...config.get(['apps', appName]), - ...opts.appConfig, + pathname: `${basePath}${appConfig.pathname}`, + hash: hash || appConfig.hash, }); log.debug('navigating to ' + appName + ' url: ' + appUrl); @@ -133,13 +162,14 @@ export function CommonPageProvider({ getService, getPageObjects }) { return browser.refresh(); }) .then(async function () { - const currentUrl = await self.loginIfPrompted(appUrl); + const currentUrl = shouldLoginIfPrompted ? await self.loginIfPrompted(appUrl) : browser.getCurrentUrl(); if (currentUrl.includes('app/kibana')) { await testSubjects.find('kibanaChrome'); } }) .then(async function () { + const currentUrl = (await browser.getCurrentUrl()).replace(/\/\/\w+:\w+@/, '//'); const maxAdditionalLengthOnNavUrl = 230; // On several test failures at the end of the TileMap test we try to navigate back to @@ -327,6 +357,11 @@ export function CommonPageProvider({ getService, getPageObjects }) { } } } + + async getBodyText() { + const el = await find.byCssSelector('body>pre'); + return await el.getVisibleText(); + } } return new CommonPage(); diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index ee7d07e5c6f339..c8ec060c246fde 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -18,7 +18,6 @@ */ import _ from 'lodash'; - import { DashboardConstants } from '../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; export const PIE_CHART_VIS_NAME = 'Visualization PieChart'; @@ -219,7 +218,7 @@ export function DashboardPageProvider({ getService, getPageObjects }) { } async clickNewDashboard() { - // newDashboardLink button is only visible when dashboard listing table is displayed + // newItemButton button is only visible when dashboard listing table is displayed // (at least one dashboard). const exists = await testSubjects.exists('newItemButton'); if (exists) { @@ -606,6 +605,10 @@ export function DashboardPageProvider({ getService, getPageObjects }) { return await testSubjects.click('dashboardPanelTitlesCheckbox'); } + async expectMissingSaveOption() { + await testSubjects.missingOrFail('dashboardSaveMenuItem'); + } + async getNotLoadedVisualizations(vizList) { const checkList = []; for (const name of vizList) { diff --git a/test/functional/page_objects/discover_page.js b/test/functional/page_objects/discover_page.js index 502dbf5de17cbb..2738482fcb521a 100644 --- a/test/functional/page_objects/discover_page.js +++ b/test/functional/page_objects/discover_page.js @@ -236,6 +236,14 @@ export function DiscoverPageProvider({ getService, getPageObjects }) { }); } + async expectFieldListItemVisualize(field) { + await testSubjects.existOrFail(`fieldVisualize-${field}`); + } + + async expectMissingFieldListItemVisualize(field) { + await testSubjects.missingOrFail(`fieldVisualize-${field}`); + } + async clickFieldListPlusFilter(field, value) { // this method requires the field details to be open from clickFieldListItem() // testSubjects.find doesn't handle spaces in the data-test-subj value diff --git a/test/functional/page_objects/error_page.js b/test/functional/page_objects/error_page.js new file mode 100644 index 00000000000000..dd4966ca690fdf --- /dev/null +++ b/test/functional/page_objects/error_page.js @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import expect from '@kbn/expect'; + +export function ErrorPageProvider({ getPageObjects }) { + const PageObjects = getPageObjects(['common']); + + class ErrorPage { + async expectForbidden() { + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 403, + error: 'Forbidden', + message: 'Forbidden' + }) + ); + } + async expectNotFound() { + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + } + } + + return new ErrorPage(); +} diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index 1c1e06d08c9dde..1e8c454f42cfe3 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -28,6 +28,8 @@ import { DashboardPageProvider } from './dashboard_page'; // @ts-ignore not TS yet import { DiscoverPageProvider } from './discover_page'; // @ts-ignore not TS yet +import { ErrorPageProvider } from './error_page'; +// @ts-ignore not TS yet import { HeaderPageProvider } from './header_page'; // @ts-ignore not TS yet import { HomePageProvider } from './home_page'; @@ -55,6 +57,7 @@ export const pageObjects = { context: ContextPageProvider, dashboard: DashboardPageProvider, discover: DiscoverPageProvider, + error: ErrorPageProvider, header: HeaderPageProvider, home: HomePageProvider, monitoring: MonitoringPageProvider, diff --git a/test/functional/page_objects/settings_page.js b/test/functional/page_objects/settings_page.js index 2b42906717f364..a25257e3ba1d10 100644 --- a/test/functional/page_objects/settings_page.js +++ b/test/functional/page_objects/settings_page.js @@ -66,6 +66,11 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return await setting.getProperty('value'); } + async expectDisabledAdvancedSetting(propertyName) { + const setting = await testSubjects.find(`advancedSetting-editField-${propertyName}`); + expect(setting.getAttribute('disabled')).to.eql(''); + } + async getAdvancedSettingCheckbox(propertyName) { log.debug('in getAdvancedSettingCheckbox'); return await testSubjects.getProperty(`advancedSetting-editField-${propertyName}`, 'checked'); @@ -607,6 +612,50 @@ export function SettingsPageProvider({ getService, getPageObjects }) { return objects; } + + async getSavedObjectsTableSummary() { + const table = await testSubjects.find('savedObjectsTable'); + const rows = await table.findAllByCssSelector('tbody tr'); + + const summary = []; + for (const row of rows) { + const titleCell = await row.findByCssSelector('td:nth-child(3)'); + const title = await titleCell.getVisibleText(); + + + const viewInAppButtons = await row.findAllByCssSelector('[aria-label="In app"]'); + const canViewInApp = Boolean(viewInAppButtons.length); + summary.push({ + title, + canViewInApp, + }); + } + + return summary; + } + + async clickSavedObjectsTableSelectAll() { + const checkboxSelectAll = await testSubjects.find('checkboxSelectAll'); + await checkboxSelectAll.click(); + } + + async canSavedObjectsBeDeleted() { + const deleteButton = await testSubjects.find('savedObjectsManagementDelete'); + return await deleteButton.isEnabled(); + } + + async canSavedObjectBeDeleted(id) { + const allCheckBoxes = await testSubjects.findAll('checkboxSelectRow*'); + for (const checkBox of allCheckBoxes) { + if (await checkBox.isSelected()) { + await checkBox.click(); + } + } + + const checkBox = await testSubjects.find(`checkboxSelectRow-${id}`); + await checkBox.click(); + return await this.canSavedObjectsBeDeleted(); + } } return new SettingsPage(); diff --git a/test/functional/page_objects/share_page.js b/test/functional/page_objects/share_page.js index b9be3faae640b8..0bd2def010274d 100644 --- a/test/functional/page_objects/share_page.js +++ b/test/functional/page_objects/share_page.js @@ -53,6 +53,14 @@ export function SharePageProvider({ getService, getPageObjects }) { return await testSubjects.getAttribute('copyShareUrlButton', 'data-share-url'); } + async createShortUrlExistOrFail() { + await testSubjects.existOrFail('createShortUrl'); + } + + async createShortUrlMissingOrFail() { + await testSubjects.missingOrFail('createShortUrl'); + } + async checkShortenUrl() { const shareForm = await testSubjects.find('shareUrlForm'); await PageObjects.visualize.checkCheckbox('useShortUrl'); diff --git a/test/functional/page_objects/timelion_page.js b/test/functional/page_objects/timelion_page.js index 7ca7af8a6ef1db..cae1343d0508c0 100644 --- a/test/functional/page_objects/timelion_page.js +++ b/test/functional/page_objects/timelion_page.js @@ -68,6 +68,22 @@ export function TimelionPageProvider({ getService, getPageObjects }) { // Wait for timelion expression to be updated after clicking suggestions await PageObjects.common.sleep(waitTime); } + + async saveTimelionSheet() { + await testSubjects.click('timelionSaveButton'); + await testSubjects.click('timelionSaveAsSheetButton'); + await testSubjects.click('timelionFinishSaveButton'); + } + + async expectWriteControls() { + await testSubjects.existOrFail('timelionSaveButton'); + await testSubjects.existOrFail('timelionDeleteButton'); + } + + async expectMissingWriteControls() { + await testSubjects.missingOrFail('timelionSaveButton'); + await testSubjects.missingOrFail('timelionDeleteButton'); + } } return new TimelionPage(); diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js index 2b0e922d698eca..9e062beaadb65d 100644 --- a/test/functional/page_objects/visualize_page.js +++ b/test/functional/page_objects/visualize_page.js @@ -764,12 +764,9 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli )); } - async saveVisualizationExpectFail(vizName, { saveAsNew = false } = {}) { - await this.saveVisualization(vizName, { saveAsNew }); - const errorToast = await testSubjects.exists('saveVisualizationError', { - timeout: defaultFindTimeout - }); - expect(errorToast).to.be(true); + async expectNoSaveOption() { + const saveButtonExists = await testSubjects.exists('visualizeSaveButton'); + expect(saveButtonExists).to.be(false); } async clickLoadSavedVisButton() { diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index ad5b2fde1acfef..5386fdc1251c39 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -13,6 +13,7 @@ export function createJestConfig({ roots: [ '/plugins', '/server', + '/public', ], moduleFileExtensions: [ 'js', diff --git a/x-pack/package.json b/x-pack/package.json index 938a14e689cdcd..caa977c2eb712d 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -87,6 +87,7 @@ "chalk": "^2.4.1", "chance": "1.0.10", "checksum": "0.1.1", + "cheerio": "0.22.0", "commander": "2.12.2", "copy-webpack-plugin": "^4.5.2", "del": "^3.0.0", diff --git a/x-pack/plugins/__mocks__/ui/capabilities.ts b/x-pack/plugins/__mocks__/ui/capabilities.ts new file mode 100644 index 00000000000000..5de2c6144fc6cf --- /dev/null +++ b/x-pack/plugins/__mocks__/ui/capabilities.ts @@ -0,0 +1,29 @@ +/* + * 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 { UICapabilities } from 'ui/capabilities'; + +let internals: UICapabilities = { + navLinks: {}, + management: {}, + catalogue: {}, + spaces: { + manage: true, + }, +}; + +export const uiCapabilities = new Proxy( + {}, + { + get: (target, property) => { + return internals[String(property)] as any; + }, + } +); + +export function setMockCapabilities(mockCapabilities: UICapabilities) { + internals = mockCapabilities; +} diff --git a/x-pack/plugins/__mocks__/ui/chrome.js b/x-pack/plugins/__mocks__/ui/chrome.js index b85d345a68ce3b..bdba07fa08dfa0 100644 --- a/x-pack/plugins/__mocks__/ui/chrome.js +++ b/x-pack/plugins/__mocks__/ui/chrome.js @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { uiCapabilities } from './capabilities'; + function getUiSettingsClient() { return { get: key => { @@ -33,6 +35,8 @@ function getInjected(key) { return 'apm*'; case 'mlEnabled': return true; + case 'uiCapabilities': + return uiCapabilities; case 'isCloudEnabled': return false; default: diff --git a/x-pack/plugins/apm/index.ts b/x-pack/plugins/apm/index.ts index 533071dde48208..a97c63a51bf1e3 100644 --- a/x-pack/plugins/apm/index.ts +++ b/x-pack/plugins/apm/index.ts @@ -70,6 +70,37 @@ export function apm(kibana: any) { // TODO: get proper types init(server: Server) { + server.plugins.xpack_main.registerFeature({ + id: 'apm', + name: i18n.translate('xpack.apm.featureRegistry.apmFeatureName', { + defaultMessage: 'APM' + }), + icon: 'apmApp', + navLinkId: 'apm', + app: ['apm', 'kibana'], + catalogue: ['apm'], + privileges: { + all: { + api: ['apm'], + catalogue: ['apm'], + savedObject: { + all: [], + read: ['config'] + }, + ui: ['show'] + }, + read: { + api: ['apm'], + catalogue: ['apm'], + savedObject: { + all: [], + read: ['config'] + }, + ui: ['show'] + } + } + }); + const initializerContext = {} as PluginInitializerContext; const core = { http: { diff --git a/x-pack/plugins/apm/public/components/app/Main/index.tsx b/x-pack/plugins/apm/public/components/app/Main/index.tsx index 908a0f02116455..a34d8da6097490 100644 --- a/x-pack/plugins/apm/public/components/app/Main/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Main/index.tsx @@ -25,7 +25,7 @@ const MainContainer = styled.div` export function Main() { return ( - + diff --git a/x-pack/plugins/apm/server/routes/errors.ts b/x-pack/plugins/apm/server/routes/errors.ts index e4f855f298e9e5..3bd7be240eb4cf 100644 --- a/x-pack/plugins/apm/server/routes/errors.ts +++ b/x-pack/plugins/apm/server/routes/errors.ts @@ -32,7 +32,8 @@ export function initErrorsApi(core: CoreSetup) { sortField: Joi.string(), sortDirection: Joi.string() }) - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -57,7 +58,8 @@ export function initErrorsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -83,7 +85,8 @@ export function initErrorsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: distributionHandler }); @@ -94,7 +97,8 @@ export function initErrorsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: distributionHandler }); diff --git a/x-pack/plugins/apm/server/routes/metrics.ts b/x-pack/plugins/apm/server/routes/metrics.ts index c94defca8a5ff2..997ff9b803c09e 100644 --- a/x-pack/plugins/apm/server/routes/metrics.ts +++ b/x-pack/plugins/apm/server/routes/metrics.ts @@ -24,7 +24,8 @@ export function initMetricsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: async req => { const setup = setupRequest(req); diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 345dda5da68c8d..0bbc1b8be894ba 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -28,7 +28,8 @@ export function initServicesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: async req => { const setup = setupRequest(req); @@ -51,7 +52,8 @@ export function initServicesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); diff --git a/x-pack/plugins/apm/server/routes/traces.ts b/x-pack/plugins/apm/server/routes/traces.ts index 112c79110d37d3..5bc996b1b71cfc 100644 --- a/x-pack/plugins/apm/server/routes/traces.ts +++ b/x-pack/plugins/apm/server/routes/traces.ts @@ -29,7 +29,8 @@ export function initTracesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -45,7 +46,8 @@ export function initTracesApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const { traceId } = req.params; diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 71edd0ba3867c6..5c93edf5bd867e 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -31,7 +31,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { query: withDefaultValidators({ query: Joi.string() }) - } + }, + tags: ['access:apm'] }, handler: req => { const { serviceName, transactionType } = req.params; @@ -51,7 +52,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -71,7 +73,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -90,7 +93,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { options: { validate: { query: withDefaultValidators() - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); @@ -114,7 +118,8 @@ export function initTransactionGroupsApi(core: CoreSetup) { transactionId: Joi.string().default(''), traceId: Joi.string().default('') }) - } + }, + tags: ['access:apm'] }, handler: req => { const setup = setupRequest(req); diff --git a/x-pack/plugins/canvas/init.js b/x-pack/plugins/canvas/init.js index ef06aa9eaafa84..9feff1fe12ac75 100644 --- a/x-pack/plugins/canvas/init.js +++ b/x-pack/plugins/canvas/init.js @@ -35,6 +35,31 @@ export default async function(server /*options*/) { }; }); + server.plugins.xpack_main.registerFeature({ + id: 'canvas', + name: 'Canvas', + icon: 'canvasApp', + navLinkId: 'canvas', + app: ['canvas', 'kibana'], + catalogue: ['canvas'], + privileges: { + all: { + savedObject: { + all: ['canvas-workpad'], + read: ['config', 'index-pattern'], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'canvas-workpad'], + }, + ui: [], + }, + }, + }); + registerCanvasUsageCollector(server); loadSampleData(server); routes(server); diff --git a/x-pack/plugins/canvas/public/apps/workpad/routes.js b/x-pack/plugins/canvas/public/apps/workpad/routes.js index 996df0be1f9071..0618a390759201 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/routes.js +++ b/x-pack/plugins/canvas/public/apps/workpad/routes.js @@ -12,8 +12,6 @@ import { setWorkpad } from '../../state/actions/workpad'; import { setAssets, resetAssets } from '../../state/actions/assets'; import { setPage } from '../../state/actions/pages'; import { getWorkpad } from '../../state/selectors/workpad'; -import { isFirstLoad } from '../../state/selectors/app'; -import { setCanUserWrite, setFirstLoad } from '../../state/actions/transient'; import { WorkpadApp } from './workpad_app'; export const routes = [ @@ -32,11 +30,6 @@ export const routes = [ router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); } catch (err) { notify.error(err, { title: `Couldn't create workpad` }); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (err.response && err.response.status === 403) { - dispatch(setCanUserWrite(false)); - } router.redirectTo('home'); } }, @@ -51,23 +44,10 @@ export const routes = [ // load workpad if given a new id via url param const state = getState(); const currentWorkpad = getWorkpad(state); - const firstLoad = isFirstLoad(state); if (params.id !== currentWorkpad.id) { try { const fetchedWorkpad = await workpadService.get(params.id); - // tests if user has permissions to write to workpads - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (firstLoad) { - await workpadService.update(params.id, fetchedWorkpad).catch(err => { - if (err.response && err.response.status === 403) { - dispatch(setCanUserWrite(false)); - } - }); - dispatch(setFirstLoad(false)); - } - const { assets, ...workpad } = fetchedWorkpad; dispatch(setWorkpad(workpad)); dispatch(setAssets(assets)); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js index 846c3f1ff5b1ed..5bbc012b4c0156 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.js @@ -141,6 +141,7 @@ export class WorkpadHeader extends React.PureComponent { fill size="s" iconType="vector" + data-test-subj="add-element-button" onClick={() => setShowElementModal(true)} > Add element diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/index.js index 31ea8f7d41e5fd..fb1150dd33904a 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/index.js @@ -12,7 +12,6 @@ import { notify } from '../../lib/notify'; import { canUserWrite } from '../../state/selectors/app'; import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; -import { setCanUserWrite } from '../../state/actions/transient'; import { downloadWorkpad } from '../../lib/download_workpad'; import { WorkpadLoader as Component } from './workpad_loader'; @@ -21,18 +20,11 @@ const mapStateToProps = state => ({ canUserWrite: canUserWrite(state), }); -const mapDispatchToProps = dispatch => ({ - setCanUserWrite: canUserWrite => dispatch(setCanUserWrite(canUserWrite)), -}); - export const WorkpadLoader = compose( getContext({ router: PropTypes.object, }), - connect( - mapStateToProps, - mapDispatchToProps - ), + connect(mapStateToProps), withState('workpads', 'setWorkpads', null), withHandlers({ // Workpad creation via navigation @@ -44,11 +36,6 @@ export const WorkpadLoader = compose( props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { notify.error(err, { title: `Couldn't upload workpad` }); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (err.response && err.response.status === 403) { - props.setCanUserWrite(false); - } } return; } @@ -79,11 +66,6 @@ export const WorkpadLoader = compose( props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); } catch (err) { notify.error(err, { title: `Couldn't clone workpad` }); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (err.response && err.response.status === 403) { - props.setCanUserWrite(false); - } } }, @@ -112,11 +94,6 @@ export const WorkpadLoader = compose( if (result.err) { errors.push(result.id); - // TODO: remove this and switch to checking user privileges when canvas loads when granular app privileges are introduced - // https://github.com/elastic/kibana/issues/20277 - if (result.err.response && result.err.response.status === 403) { - props.setCanUserWrite(false); - } } else { passes.push(result.id); } diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js index 8feedeb7546d59..ad46f25669e8a2 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_create.js @@ -9,7 +9,14 @@ import PropTypes from 'prop-types'; import { EuiButton } from '@elastic/eui'; export const WorkpadCreate = ({ createPending, onCreate, ...rest }) => ( - + Create workpad ); diff --git a/x-pack/plugins/canvas/public/state/actions/transient.js b/x-pack/plugins/canvas/public/state/actions/transient.js index 20e467ff06fe87..a87c39b7ef6e02 100644 --- a/x-pack/plugins/canvas/public/state/actions/transient.js +++ b/x-pack/plugins/canvas/public/state/actions/transient.js @@ -6,7 +6,6 @@ import { createAction } from 'redux-actions'; -export const setCanUserWrite = createAction('setCanUserWrite'); export const setFullscreen = createAction('setFullscreen'); export const selectToplevelNodes = createAction('selectToplevelNodes'); export const setFirstLoad = createAction('setFirstLoad'); diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index ade6f46985080b..bb814f8c30d99b 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -5,6 +5,7 @@ */ import { get } from 'lodash'; +import { uiCapabilities } from 'ui/capabilities'; import { getDefaultWorkpad } from './defaults'; export const getInitialState = path => { @@ -12,8 +13,7 @@ export const getInitialState = path => { app: {}, // Kibana stuff in here assets: {}, // assets end up here transient: { - isFirstLoad: true, - canUserWrite: true, + canUserWrite: uiCapabilities.canvas.save, elementStats: { total: 0, ready: 0, diff --git a/x-pack/plugins/canvas/public/state/reducers/transient.js b/x-pack/plugins/canvas/public/state/reducers/transient.js index bb40404da48840..43b2c9ccc188ca 100644 --- a/x-pack/plugins/canvas/public/state/reducers/transient.js +++ b/x-pack/plugins/canvas/public/state/reducers/transient.js @@ -29,10 +29,6 @@ export const transientReducer = handleActions( ); }, - [transientActions.setCanUserWrite]: (transientState, { payload }) => { - return set(transientState, 'canUserWrite', Boolean(payload)); - }, - [transientActions.setFirstLoad]: (transientState, { payload }) => { return set(transientState, 'isFirstLoad', Boolean(payload)); }, diff --git a/x-pack/plugins/canvas/public/state/selectors/app.js b/x-pack/plugins/canvas/public/state/selectors/app.js index 26e5fff11b7860..0f13df85a03533 100644 --- a/x-pack/plugins/canvas/public/state/selectors/app.js +++ b/x-pack/plugins/canvas/public/state/selectors/app.js @@ -11,10 +11,6 @@ export function canUserWrite(state) { return get(state, 'transient.canUserWrite', true); } -export function isFirstLoad(state) { - return get(state, 'transient.isFirstLoad', true); -} - export function getFullscreen(state) { return get(state, 'transient.fullscreen', false); } diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js index 26175c37f69f0e..8024696f53d922 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -14,6 +14,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js index 698a20cbd8f08b..9ec603087be85b 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -15,6 +15,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js index f7617bb7973820..53aeaebdd314a5 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js @@ -17,6 +17,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js index 80febd94af6721..b45750a37f1843 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js @@ -15,6 +15,15 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js index 3c8372ffba8dc0..9e87b013394ddd 100644 --- a/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js @@ -14,6 +14,16 @@ import routing from '../../public/app/services/routing'; jest.mock('ui/chrome', () => ({ addBasePath: () => 'api/cross_cluster_replication', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + } })); jest.mock('ui/index_patterns', () => { diff --git a/x-pack/plugins/graph/index.js b/x-pack/plugins/graph/index.js index aa11b433475c69..d2799157c99c1d 100644 --- a/x-pack/plugins/graph/index.js +++ b/x-pack/plugins/graph/index.js @@ -5,7 +5,7 @@ */ import { resolve } from 'path'; -import Boom from 'boom'; +import { i18n } from '@kbn/i18n'; import migrations from './migrations'; import { initServer } from './server'; @@ -49,6 +49,33 @@ export function graph(kibana) { }; }); + server.plugins.xpack_main.registerFeature({ + id: 'graph', + name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { + defaultMessage: 'Graph', + }), + icon: 'graphApp', + navLinkId: 'graph', + app: ['graph', 'kibana'], + catalogue: ['graph'], + privileges: { + all: { + savedObject: { + all: ['graph-workspace'], + read: ['config', 'index-pattern'], + }, + ui: ['save', 'delete'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'graph-workspace'], + }, + ui: [], + } + } + }); + initServer(server); }, }); diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js index 305a229a80fceb..b5bbfa5d5c5c19 100644 --- a/x-pack/plugins/graph/public/app.js +++ b/x-pack/plugins/graph/public/app.js @@ -47,6 +47,7 @@ import { import { getOutlinkEncoders, } from './services/outlink_encoders'; +import { uiCapabilities } from 'ui/capabilities'; const app = uiModules.get('app/graph'); @@ -796,35 +797,43 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private }), run: function () {canWipeWorkspace(function () {kbnUrl.change('/home', {}); }); }, }); - if (!$scope.allSavingDisabled) { - $scope.topNavMenu.push({ - key: 'save', - label: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', - }), - description: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', - }), - disableButton: function () {return $scope.selectedFields.length === 0;}, - template: require('./templates/save_workspace.html') - }); - }else { - $scope.topNavMenu.push({ - key: 'save', - label: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledLabel', { - defaultMessage: 'Save', - }), - description: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledAriaLabel', { - defaultMessage: 'Save Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }), - disableButton: true - }); + + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (uiCapabilities.graph.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + if (!$scope.allSavingDisabled) { + $scope.topNavMenu.push({ + key: 'save', + label: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }), + disableButton: function () {return $scope.selectedFields.length === 0;}, + template: require('./templates/save_workspace.html'), + testId: 'graphSaveButton', + }); + } else { + $scope.topNavMenu.push({ + key: 'save', + label: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledLabel', { + defaultMessage: 'Save', + }), + description: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledAriaLabel', { + defaultMessage: 'Save Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }), + disableButton: true, + testId: 'graphSaveButton', + }); + } } $scope.topNavMenu.push({ key: 'open', @@ -837,65 +846,74 @@ app.controller('graphuiPlugin', function ($scope, $route, $http, kbnUrl, Private tooltip: i18n('xpack.graph.topNavMenu.loadWorkspaceTooltip', { defaultMessage: 'Load a saved workspace', }), - template: require('./templates/load_workspace.html') + template: require('./templates/load_workspace.html'), + testId: 'graphOpenButton', }); - if (!$scope.allSavingDisabled) { - $scope.topNavMenu.push({ - key: 'delete', - disableButton: function () { - return $route.current.locals === undefined || $route.current.locals.savedWorkspace === undefined; - }, - label: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledLabel', { - defaultMessage: 'Delete', - }), - description: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaLabel', { - defaultMessage: 'Delete Saved Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaTooltip', { - defaultMessage: 'Delete this workspace', - }), - run: function () { - const title = $route.current.locals.savedWorkspace.title; - function doDelete() { - $route.current.locals.SavedWorkspacesProvider.delete($route.current.locals.savedWorkspace.id); - kbnUrl.change('/home', {}); - - toastNotifications.addSuccess( - i18n('xpack.graph.topNavMenu.deleteWorkspaceNotification', { - defaultMessage: `Deleted '{workspaceTitle}'`, - values: { workspaceTitle: title }, - }) + // if deleting is disabled using uiCapabilities, we don't want to render the delete + // button so it's consistent with all of the other applications + if (uiCapabilities.graph.delete) { + + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + if (!$scope.allSavingDisabled) { + $scope.topNavMenu.push({ + key: 'delete', + disableButton: function () { + return $route.current.locals === undefined || $route.current.locals.savedWorkspace === undefined; + }, + label: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledLabel', { + defaultMessage: 'Delete', + }), + description: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaLabel', { + defaultMessage: 'Delete Saved Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.enabledAriaTooltip', { + defaultMessage: 'Delete this workspace', + }), + testId: 'graphDeleteButton', + run: function () { + const title = $route.current.locals.savedWorkspace.title; + function doDelete() { + $route.current.locals.SavedWorkspacesProvider.delete($route.current.locals.savedWorkspace.id); + kbnUrl.change('/home', {}); + + toastNotifications.addSuccess( + i18n('xpack.graph.topNavMenu.deleteWorkspaceNotification', { + defaultMessage: `Deleted '{workspaceTitle}'`, + values: { workspaceTitle: title }, + }) + ); + } + const confirmModalOptions = { + onConfirm: doDelete, + confirmButtonText: i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmButtonLabel', { + defaultMessage: 'Delete workspace', + }), + }; + confirmModal( + i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmText', { + defaultMessage: 'Are you sure you want to delete the workspace {title} ?', + values: { title }, + }), + confirmModalOptions ); } - const confirmModalOptions = { - onConfirm: doDelete, - confirmButtonText: i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmButtonLabel', { - defaultMessage: 'Delete workspace', - }), - }; - confirmModal( - i18n('xpack.graph.topNavMenu.deleteWorkspace.confirmText', { - defaultMessage: 'Are you sure you want to delete the workspace {title} ?', - values: { title }, - }), - confirmModalOptions - ); - } - }); - }else { - $scope.topNavMenu.push({ - key: 'delete', - disableButton: true, - label: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledLabel', { - defaultMessage: 'Delete', - }), - description: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledAriaLabel', { - defaultMessage: 'Delete Saved Workspace', - }), - tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledTooltip', { - defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', - }), - }); + }); + }else { + $scope.topNavMenu.push({ + key: 'delete', + disableButton: true, + label: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledLabel', { + defaultMessage: 'Delete', + }), + description: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledAriaLabel', { + defaultMessage: 'Delete Saved Workspace', + }), + tooltip: i18n('xpack.graph.topNavMenu.deleteWorkspace.disabledTooltip', { + defaultMessage: 'No changes to saved workspaces are permitted by the current save policy', + }), + testId: 'graphDeleteButton', + }); + } } $scope.topNavMenu.push({ key: 'settings', diff --git a/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js b/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js index 74530e73f379e8..aa05638403a960 100644 --- a/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js +++ b/x-pack/plugins/grokdebugger/public/sections/grokdebugger/grokdebugger_route.js @@ -5,14 +5,17 @@ */ import routes from 'ui/routes'; +import 'ui/capabilities/route_setup'; import { toastNotifications } from 'ui/notify'; import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info'; import template from './grokdebugger_route.html'; import './directives/grokdebugger'; + routes .when('/dev_tools/grokdebugger', { template: template, + requireUICapability: 'dev_tools.show', resolve: { licenseCheckResults(Private) { const xpackInfo = Private(XPackInfoProvider); diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index c20c423b700680..96cac959ced756 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -14,6 +14,7 @@ import { pluck } from 'rxjs/operators'; // TODO use theme provided from parentApp when kibana supports it import { EuiErrorBoundary } from '@elastic/eui'; +import { UICapabilitiesProvider } from 'ui/capabilities/react'; import { I18nContext } from 'ui/i18n'; import { EuiThemeProvider } from '../../../../common/eui_styled_components'; import { InfraFrontendLibs } from '../lib/lib'; @@ -36,19 +37,21 @@ export async function startApp(libs: InfraFrontendLibs) { return ( - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx index fae8a65b3cf642..c48562b355d837 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/fields_configuration_panel.tsx @@ -14,6 +14,7 @@ interface FieldsConfigurationPanelProps { containerFieldProps: InputFieldProps; hostFieldProps: InputFieldProps; isLoading: boolean; + readOnly: boolean; podFieldProps: InputFieldProps; tiebreakerFieldProps: InputFieldProps; timestampFieldProps: InputFieldProps; @@ -23,6 +24,7 @@ export const FieldsConfigurationPanel = ({ containerFieldProps, hostFieldProps, isLoading, + readOnly, podFieldProps, tiebreakerFieldProps, timestampFieldProps, @@ -57,7 +59,13 @@ export const FieldsConfigurationPanel = ({ /> } > - + @@ -106,7 +115,13 @@ export const FieldsConfigurationPanel = ({ /> } > - + } > - + } > - + ); diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx index 0f0489594d561e..3ea0c2ca704c2e 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_panel.tsx @@ -12,12 +12,14 @@ import { InputFieldProps } from './source_configuration_form_state'; interface IndicesConfigurationPanelProps { isLoading: boolean; + readOnly: boolean; logAliasFieldProps: InputFieldProps; metricAliasFieldProps: InputFieldProps; } export const IndicesConfigurationPanel = ({ isLoading, + readOnly, logAliasFieldProps, metricAliasFieldProps, }: IndicesConfigurationPanelProps) => ( @@ -55,6 +57,7 @@ export const IndicesConfigurationPanel = ({ data-test-subj="metricIndicesInput" fullWidth disabled={isLoading} + readOnly={readOnly} isLoading={isLoading} {...metricAliasFieldProps} /> @@ -84,6 +87,7 @@ export const IndicesConfigurationPanel = ({ fullWidth disabled={isLoading} isLoading={isLoading} + readOnly={readOnly} {...logAliasFieldProps} /> diff --git a/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx b/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx index 93f2d03e83cbf4..99db9c040ed29e 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/name_configuration_panel.tsx @@ -12,11 +12,13 @@ import { InputFieldProps } from './source_configuration_form_state'; interface NameConfigurationPanelProps { isLoading: boolean; + readOnly: boolean; nameFieldProps: InputFieldProps; } export const NameConfigurationPanel = ({ isLoading, + readOnly, nameFieldProps, }: NameConfigurationPanelProps) => ( @@ -41,6 +43,7 @@ export const NameConfigurationPanel = ({ data-test-subj="nameInput" fullWidth disabled={isLoading} + readOnly={readOnly} isLoading={isLoading} {...nameFieldProps} /> diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx index 5416d34a208e13..d5f3e0f3ada882 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_flyout.tsx @@ -28,7 +28,14 @@ import { useSourceConfigurationFormState } from './source_configuration_form_sta const noop = () => undefined; -export const SourceConfigurationFlyout: React.FunctionComponent = () => { +interface SourceConfigurationFlyoutProps { + shouldAllowEdit: boolean; +} + +export const SourceConfigurationFlyout: React.FunctionComponent< + SourceConfigurationFlyoutProps +> = props => { + const { shouldAllowEdit } = props; const { isVisible, hide } = useContext(SourceConfigurationFlyoutState.Context); const { @@ -98,18 +105,30 @@ export const SourceConfigurationFlyout: React.FunctionComponent = () => {

- + {shouldAllowEdit ? ( + + ) : ( + + )}

- + @@ -118,6 +137,7 @@ export const SourceConfigurationFlyout: React.FunctionComponent = () => { containerFieldProps={fieldProps.containerField} hostFieldProps={fieldProps.hostField} isLoading={isLoading} + readOnly={!shouldAllowEdit} podFieldProps={fieldProps.podField} tiebreakerFieldProps={fieldProps.tiebreakerField} timestampFieldProps={fieldProps.timestampField} @@ -157,26 +177,28 @@ export const SourceConfigurationFlyout: React.FunctionComponent = () => { )} - - {isLoading ? ( - - Loading - - ) : ( - - - - )} - + {shouldAllowEdit && ( + + {isLoading ? ( + + Loading + + ) : ( + + + + )} + + )} diff --git a/x-pack/plugins/infra/public/components/waffle/node.tsx b/x-pack/plugins/infra/public/components/waffle/node.tsx index 60baefb9514478..dfd622732e00aa 100644 --- a/x-pack/plugins/infra/public/components/waffle/node.tsx +++ b/x-pack/plugins/infra/public/components/waffle/node.tsx @@ -79,6 +79,7 @@ export const Node = injectI18n( > diff --git a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx index e8918854c234d3..7e6f62a733d6cd 100644 --- a/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx +++ b/x-pack/plugins/infra/public/components/waffle/node_context_menu.tsx @@ -7,7 +7,8 @@ import { EuiContextMenu, EuiContextMenuPanelDescriptor, EuiPopover } from '@elastic/eui'; import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React from 'react'; - +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { InfraNodeType, InfraTimerangeInput } from '../../graphql/types'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib'; import { getNodeDetailUrl, getNodeLogsUrl } from '../../pages/link_to'; @@ -21,89 +22,107 @@ interface Props { isPopoverOpen: boolean; closePopover: () => void; intl: InjectedIntl; + uiCapabilities: UICapabilities; } -export const NodeContextMenu = injectI18n( - ({ options, timeRange, children, node, isPopoverOpen, closePopover, nodeType, intl }: Props) => { - // Due to the changing nature of the fields between APM and this UI, - // We need to have some exceptions until 7.0 & ECS is finalized. Reference - // #26620 for the details for these fields. - // TODO: This is tech debt, remove it after 7.0 & ECS migration. - const APM_FIELDS = { - [InfraNodeType.host]: 'host.hostname', - [InfraNodeType.container]: 'container.id', - [InfraNodeType.pod]: 'kubernetes.pod.uid', - }; +export const NodeContextMenu = injectUICapabilities( + injectI18n( + ({ + options, + timeRange, + children, + node, + isPopoverOpen, + closePopover, + nodeType, + intl, + uiCapabilities, + }: Props) => { + // Due to the changing nature of the fields between APM and this UI, + // We need to have some exceptions until 7.0 & ECS is finalized. Reference + // #26620 for the details for these fields. + // TODO: This is tech debt, remove it after 7.0 & ECS migration. + const APM_FIELDS = { + [InfraNodeType.host]: 'host.hostname', + [InfraNodeType.container]: 'container.id', + [InfraNodeType.pod]: 'kubernetes.pod.uid', + }; + + const nodeLogsUrl = + node.id && uiCapabilities.logs.show + ? getNodeLogsUrl({ + nodeType, + nodeId: node.id, + time: timeRange.to, + }) + : undefined; + const nodeDetailUrl = node.id + ? getNodeDetailUrl({ + nodeType, + nodeId: node.id, + from: timeRange.from, + to: timeRange.to, + }) + : undefined; - const nodeLogsUrl = node.id - ? getNodeLogsUrl({ - nodeType, - nodeId: node.id, - time: timeRange.to, - }) - : undefined; - const nodeDetailUrl = node.id - ? getNodeDetailUrl({ - nodeType, - nodeId: node.id, - from: timeRange.from, - to: timeRange.to, - }) - : undefined; + const apmTracesUrl = uiCapabilities.apm.show + ? { + name: intl.formatMessage( + { + id: 'xpack.infra.nodeContextMenu.viewAPMTraces', + defaultMessage: 'View {nodeType} APM traces', + }, + { nodeType } + ), + href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}~20~3A~20~22${node.id}~22`, + 'data-test-subj': 'viewApmTracesContextMenuItem', + } + : undefined; - const apmTracesUrl = { - name: intl.formatMessage( + const panels: EuiContextMenuPanelDescriptor[] = [ { - id: 'xpack.infra.nodeContextMenu.viewAPMTraces', - defaultMessage: 'View {nodeType} APM traces', + id: 0, + title: '', + items: [ + ...(nodeLogsUrl + ? [ + { + name: intl.formatMessage({ + id: 'xpack.infra.nodeContextMenu.viewLogsName', + defaultMessage: 'View logs', + }), + href: nodeLogsUrl, + 'data-test-subj': 'viewLogsContextMenuItem', + }, + ] + : []), + ...(nodeDetailUrl + ? [ + { + name: intl.formatMessage({ + id: 'xpack.infra.nodeContextMenu.viewMetricsName', + defaultMessage: 'View metrics', + }), + href: nodeDetailUrl, + }, + ] + : []), + ...(apmTracesUrl ? [apmTracesUrl] : []), + ], }, - { nodeType } - ), - href: `../app/apm#/traces?_g=()&kuery=${APM_FIELDS[nodeType]}~20~3A~20~22${node.id}~22`, - }; - - const panels: EuiContextMenuPanelDescriptor[] = [ - { - id: 0, - title: '', - items: [ - ...(nodeLogsUrl - ? [ - { - name: intl.formatMessage({ - id: 'xpack.infra.nodeContextMenu.viewLogsName', - defaultMessage: 'View logs', - }), - href: nodeLogsUrl, - }, - ] - : []), - ...(nodeDetailUrl - ? [ - { - name: intl.formatMessage({ - id: 'xpack.infra.nodeContextMenu.viewMetricsName', - defaultMessage: 'View metrics', - }), - href: nodeDetailUrl, - }, - ] - : []), - ...[apmTracesUrl], - ], - }, - ]; + ]; - return ( - - - - ); - } + return ( + + + + ); + } + ) ); diff --git a/x-pack/plugins/infra/public/pages/404.tsx b/x-pack/plugins/infra/public/pages/404.tsx index d99d7339c79d75..631c6d8299e08d 100644 --- a/x-pack/plugins/infra/public/pages/404.tsx +++ b/x-pack/plugins/infra/public/pages/404.tsx @@ -10,7 +10,7 @@ import React from 'react'; export class NotFoundPage extends React.PureComponent { public render() { return ( -
+
{ - const { show } = useContext(SourceConfigurationFlyoutState.Context); - const { - derivedIndexPattern, - hasFailedLoadingSource, - isLoading, - loadSourceFailureMessage, - loadSource, - metricIndicesExist, - } = useContext(Source.Context); +interface SnapshotPageProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} + +export const SnapshotPage = injectUICapabilities( + injectI18n((props: SnapshotPageProps) => { + const { intl, uiCapabilities } = props; + const { show } = useContext(SourceConfigurationFlyoutState.Context); + const { + derivedIndexPattern, + hasFailedLoadingSource, + isLoading, + loadSourceFailureMessage, + loadSource, + metricIndicesExist, + } = useContext(Source.Context); - return ( - - - intl.formatMessage( + return ( + + + intl.formatMessage( + { + id: 'xpack.infra.infrastructureSnapshotPage.documentTitle', + defaultMessage: '{previousTitle} | Snapshot', + }, + { + previousTitle, + } + ) + } + /> +
-
- - {isLoading ? ( - - ) : metricIndicesExist ? ( - <> - - - - - - - ) : hasFailedLoadingSource ? ( - - ) : ( - - {({ basePath }) => ( - - - - {intl.formatMessage({ - id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', - defaultMessage: 'View setup instructions', - })} - - - - - {intl.formatMessage({ - id: 'xpack.infra.configureSourceActionLabel', - defaultMessage: 'Change source configuration', - })} - - - - } - data-test-subj="noMetricsIndicesPrompt" - /> - )} - - )} - - ); -}); + ]} + /> + + {isLoading ? ( + + ) : metricIndicesExist ? ( + <> + + + + + + + ) : hasFailedLoadingSource ? ( + + ) : ( + + {({ basePath }) => ( + + + + {intl.formatMessage({ + id: 'xpack.infra.homePage.noMetricsIndicesInstructionsActionLabel', + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities.infrastructure.configureSource ? ( + + + {intl.formatMessage({ + id: 'xpack.infra.configureSourceActionLabel', + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + data-test-subj="noMetricsIndicesPrompt" + /> + )} + + )} + + ); + }) +); diff --git a/x-pack/plugins/infra/public/pages/logs/page.tsx b/x-pack/plugins/infra/public/pages/logs/page.tsx index 8090316d98d33f..d2556bdffe8ef6 100644 --- a/x-pack/plugins/infra/public/pages/logs/page.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page.tsx @@ -13,7 +13,7 @@ import { LogsPageProviders } from './page_providers'; export const LogsPage = () => ( - + diff --git a/x-pack/plugins/infra/public/pages/logs/page_header.tsx b/x-pack/plugins/infra/public/pages/logs/page_header.tsx index a3acb34bf25c1e..6f028ea6d643ee 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_header.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_header.tsx @@ -4,41 +4,53 @@ * you may not use this file except in compliance with the Elastic License. */ -import { injectI18n } from '@kbn/i18n/react'; +import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; import React from 'react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { DocumentTitle } from '../../components/document_title'; import { Header } from '../../components/header'; import { HelpCenterContent } from '../../components/help_center_content'; import { SourceConfigurationFlyout } from '../../components/source_configuration'; -export const LogsPageHeader = injectI18n(({ intl }) => { - return ( - <> -
- - - - - ); -}); +interface LogsPageHeaderProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} + +export const LogsPageHeader = injectUICapabilities( + injectI18n((props: LogsPageHeaderProps) => { + const { intl, uiCapabilities } = props; + return ( + <> +
+ + + + + ); + }) +); diff --git a/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx index 44cda7fc97aa4b..d2482479103edf 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_no_indices_content.tsx @@ -5,55 +5,72 @@ */ import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { injectI18n } from '@kbn/i18n/react'; +import { injectI18n, InjectedIntl } from '@kbn/i18n/react'; import React, { useContext } from 'react'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { NoIndices } from '../../components/empty_states/no_indices'; import { SourceConfigurationFlyoutState } from '../../components/source_configuration'; import { WithKibanaChrome } from '../../containers/with_kibana_chrome'; -export const LogsPageNoIndicesContent = injectI18n(({ intl }) => { - const { show } = useContext(SourceConfigurationFlyoutState.Context); +interface LogsPageNoIndicesContentProps { + intl: InjectedIntl; + uiCapabilities: UICapabilities; +} - return ( - - {({ basePath }) => ( - - - - {intl.formatMessage({ - id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', - defaultMessage: 'View setup instructions', - })} - - - - - {intl.formatMessage({ - id: 'xpack.infra.configureSourceActionLabel', - defaultMessage: 'Change source configuration', - })} - - - - } - /> - )} - - ); -}); +export const LogsPageNoIndicesContent = injectUICapabilities( + injectI18n((props: LogsPageNoIndicesContentProps) => { + const { intl, uiCapabilities } = props; + const { show } = useContext(SourceConfigurationFlyoutState.Context); + + return ( + + {({ basePath }) => ( + + + + {intl.formatMessage({ + id: 'xpack.infra.logsPage.noLoggingIndicesInstructionsActionLabel', + defaultMessage: 'View setup instructions', + })} + + + {uiCapabilities.logs.configureSource ? ( + + + {intl.formatMessage({ + id: 'xpack.infra.configureSourceActionLabel', + defaultMessage: 'Change source configuration', + })} + + + ) : null} + + } + /> + )} + + ); + }) +); diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index 2d5ac5b1c19226..eeb58369ff9a31 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -16,7 +16,8 @@ import { import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { GraphQLFormattedError } from 'graphql'; import React from 'react'; - +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import euiStyled, { EuiTheme, withTheme } from '../../../../../common/eui_styled_components'; import { InfraMetricsErrorCodes } from '../../../common/errors'; import { AutoSizer } from '../../components/auto_sizer'; @@ -59,200 +60,207 @@ interface Props { }; }; intl: InjectedIntl; + uiCapabilities: UICapabilities; } -export const MetricDetail = withTheme( - injectI18n( - class extends React.PureComponent { - public static displayName = 'MetricDetailPage'; +export const MetricDetail = injectUICapabilities( + withTheme( + injectI18n( + class extends React.PureComponent { + public static displayName = 'MetricDetailPage'; - public render() { - const { intl } = this.props; - const nodeId = this.props.match.params.node; - const nodeType = this.props.match.params.type as InfraNodeType; - const layoutCreator = layoutCreators[nodeType]; - if (!layoutCreator) { - return ( - - ); - } - const layouts = layoutCreator(this.props.theme); + public render() { + const { intl, uiCapabilities } = this.props; + const nodeId = this.props.match.params.node; + const nodeType = this.props.match.params.type as InfraNodeType; + const layoutCreator = layoutCreators[nodeType]; + if (!layoutCreator) { + return ( + + ); + } + const layouts = layoutCreator(this.props.theme); - return ( - - - {({ sourceId }) => ( - - {({ - timeRange, - setTimeRange, - refreshInterval, - setRefreshInterval, - isAutoReloading, - setAutoReload, - }) => ( - - {({ name, filteredLayouts, loading: metadataLoading }) => { - const breadcrumbs = [ - { - href: '#/', - text: intl.formatMessage({ - id: 'xpack.infra.header.infrastructureTitle', - defaultMessage: 'Infrastructure', - }), - }, - { text: name }, - ]; - return ( - -
- - - + + {({ sourceId }) => ( + + {({ + timeRange, + setTimeRange, + refreshInterval, + setRefreshInterval, + isAutoReloading, + setAutoReload, + }) => ( + + {({ name, filteredLayouts, loading: metadataLoading }) => { + const breadcrumbs = [ + { + href: '#/', + text: intl.formatMessage({ + id: 'xpack.infra.header.infrastructureTitle', + defaultMessage: 'Infrastructure', + }), + }, + { text: name }, + ]; + return ( + +
+ - - - {({ metrics, error, loading, refetch }) => { - if (error) { - const invalidNodeError = error.graphQLErrors.some( - (err: GraphQLFormattedError) => - err.code === InfraMetricsErrorCodes.invalid_node - ); + /> + + + + + {({ metrics, error, loading, refetch }) => { + if (error) { + const invalidNodeError = error.graphQLErrors.some( + (err: GraphQLFormattedError) => + err.code === InfraMetricsErrorCodes.invalid_node + ); + return ( + <> + + intl.formatMessage( + { + id: + 'xpack.infra.metricDetailPage.documentTitleError', + defaultMessage: '{previousTitle} | Uh oh', + }, + { + previousTitle, + } + ) + } + /> + {invalidNodeError ? ( + + ) : ( + + )} + + ); + } return ( - <> - - intl.formatMessage( - { - id: - 'xpack.infra.metricDetailPage.documentTitleError', - defaultMessage: '{previousTitle} | Uh oh', - }, - { - previousTitle, - } - ) - } + + - {invalidNodeError ? ( - - ) : ( - - )} - + + {({ measureRef, bounds: { width = 0 } }) => { + return ( + + + + + + + +

{name}

+
+
+ +
+
+
+ + + 0 && isAutoReloading + ? false + : loading + } + refetch={refetch} + onChangeRangeTime={setTimeRange} + isLiveStreaming={isAutoReloading} + stopLiveStreaming={() => setAutoReload(false)} + /> + +
+
+ ); + }} +
+
); - } - return ( - - - - {({ measureRef, bounds: { width = 0 } }) => { - return ( - - - - - - - -

{name}

-
-
- -
-
-
+ }} +
+
+ + ); + }} + + )} + + )} + + + ); + } - - 0 && isAutoReloading - ? false - : loading - } - refetch={refetch} - onChangeRangeTime={setTimeRange} - isLiveStreaming={isAutoReloading} - stopLiveStreaming={() => setAutoReload(false)} - /> - - - - ); - }} - - - ); - }} -
-
- - ); - }} - - )} - - )} - - - ); + private handleClick = (section: InfraMetricLayoutSection) => () => { + const id = section.linkToId || section.id; + const el = document.getElementById(id); + if (el) { + el.scrollIntoView(); + } + }; } - - private handleClick = (section: InfraMetricLayoutSection) => () => { - const id = section.linkToId || section.id; - const el = document.getElementById(id); - if (el) { - el.scrollIntoView(); - } - }; - } + ) ) ); diff --git a/x-pack/plugins/infra/public/routes.tsx b/x-pack/plugins/infra/public/routes.tsx index 12328f8c601aaa..5a89d73bc2784d 100644 --- a/x-pack/plugins/infra/public/routes.tsx +++ b/x-pack/plugins/infra/public/routes.tsx @@ -8,6 +8,8 @@ import { History } from 'history'; import React from 'react'; import { Redirect, Route, Router, Switch } from 'react-router-dom'; +import { UICapabilities } from 'ui/capabilities'; +import { injectUICapabilities } from 'ui/capabilities/react'; import { NotFoundPage } from './pages/404'; import { InfrastructurePage } from './pages/infrastructure'; import { LinkToPage } from './pages/link_to'; @@ -16,21 +18,34 @@ import { MetricDetail } from './pages/metrics'; interface RouterProps { history: History; + uiCapabilities: UICapabilities; } -export const PageRouter: React.SFC = ({ history }) => { +const PageRouterComponent: React.SFC = ({ history, uiCapabilities }) => { return ( - - - - - + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.infrastructure.show && ( + + )} + {uiCapabilities.logs.show && } + {uiCapabilities.infrastructure.show && ( + + )} - + {uiCapabilities.infrastructure.show && ( + + )} ); }; + +export const PageRouter = injectUICapabilities(PageRouterComponent); diff --git a/x-pack/plugins/infra/server/kibana.index.ts b/x-pack/plugins/infra/server/kibana.index.ts index e463e8dcb59549..20480aa93b3136 100644 --- a/x-pack/plugins/infra/server/kibana.index.ts +++ b/x-pack/plugins/infra/server/kibana.index.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { Server } from 'hapi'; import JoiNamespace from 'joi'; import { initInfraServer } from './infra_server'; @@ -20,6 +21,65 @@ export const initServerWithKibana = (kbnServer: KbnServer) => { // Register a function with server to manage the collection of usage stats kbnServer.usage.collectorSet.register(UsageCollector.getUsageCollector(kbnServer)); + + const xpackMainPlugin = kbnServer.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: 'infrastructure', + name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', { + defaultMessage: 'Infrastructure', + }), + icon: 'infraApp', + navLinkId: 'infra:home', + app: ['infra', 'kibana'], + catalogue: ['infraops'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['config'], + }, + ui: ['show', 'configureSource'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['config', 'infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, + }); + + xpackMainPlugin.registerFeature({ + id: 'logs', + name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', { + defaultMessage: 'Logs', + }), + icon: 'loggingApp', + navLinkId: 'infra:logs', + app: ['infra', 'kibana'], + catalogue: ['infralogging'], + privileges: { + all: { + api: ['infra'], + savedObject: { + all: ['infrastructure-ui-source'], + read: ['config'], + }, + ui: ['show', 'configureSource'], + }, + read: { + api: ['infra'], + savedObject: { + all: [], + read: ['config', 'infrastructure-ui-source'], + }, + ui: ['show'], + }, + }, + }); }; export const getConfigSchema = (Joi: typeof JoiNamespace) => { diff --git a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts index 8b13cea43065eb..ae276229b30f04 100644 --- a/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -56,6 +56,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework schema, }), path: routePath, + route: { + tags: ['access:infra'], + }, }, plugin: graphqlHapi, }); @@ -67,6 +70,9 @@ export class InfraKibanaBackendFrameworkAdapter implements InfraBackendFramework passHeader: `'kbn-version': '${this.version}'`, }), path: `${routePath}/graphiql`, + route: { + tags: ['access:infra'], + }, }, plugin: graphiqlHapi, }); diff --git a/x-pack/plugins/maps/index.js b/x-pack/plugins/maps/index.js index 6bd0591f1ac6ac..2e97a184e5804f 100644 --- a/x-pack/plugins/maps/index.js +++ b/x-pack/plugins/maps/index.js @@ -79,6 +79,33 @@ export function maps(kibana) { const xpackMainPlugin = server.plugins.xpack_main; let routesInitialized = false; + xpackMainPlugin.registerFeature({ + id: 'maps', + name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { + defaultMessage: 'Maps', + }), + icon: APP_ICON, + navLinkId: 'maps', + app: [APP_ID, 'kibana'], + catalogue: ['maps'], + privileges: { + all: { + savedObject: { + all: ['map'], + read: ['config', 'index-pattern'] + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['map', 'config', 'index-pattern'] + }, + ui: [], + }, + } + }); + watchStatusAndLicenseToInitialize(xpackMainPlugin, this, async license => { if (license && license.maps && !routesInitialized) { diff --git a/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html index 38c3a2b5c69fc1..ed787edec6e018 100644 --- a/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html +++ b/x-pack/plugins/maps/public/angular/listing_ng_wrapper.html @@ -2,4 +2,5 @@ find="find" delete="delete" listing-limit="listingLimit" + read-only="readOnly" /> diff --git a/x-pack/plugins/maps/public/angular/map_controller.js b/x-pack/plugins/maps/public/angular/map_controller.js index c611cd8f0c2714..00b5a1005f6fc8 100644 --- a/x-pack/plugins/maps/public/angular/map_controller.js +++ b/x-pack/plugins/maps/public/angular/map_controller.js @@ -10,6 +10,7 @@ import 'ui/listen'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { uiCapabilities } from 'ui/capabilities'; import { render, unmountComponentAtNode } from 'react-dom'; import { uiModules } from 'ui/modules'; import { timefilter } from 'ui/timefilter'; @@ -31,6 +32,7 @@ import { getIsFullScreen, updateFlyout, FLYOUT_STATE, + setReadOnly, setIsLayerTOCOpen } from '../store/ui'; import { getUniqueIndexPatternIds } from '../selectors/map_selectors'; @@ -125,6 +127,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage // clear old UI state store.dispatch(setSelectedLayer(null)); store.dispatch(updateFlyout(FLYOUT_STATE.NONE)); + store.dispatch(setReadOnly(!uiCapabilities.maps.save)); handleStoreChanges(store); unsubscribe = store.subscribe(() => { @@ -294,7 +297,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage const inspectorAdapters = getInspectorAdapters(store.getState()); Inspector.open(inspectorAdapters, {}); } - }, { + }, ...(uiCapabilities.maps.save ? [{ key: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', { defaultMessage: `save` }), @@ -331,5 +334,7 @@ app.controller('GisMapController', ($scope, $route, config, kbnUrl, localStorage />); showSaveModal(saveModal); } - }]; + }] : []) + ]; }); + diff --git a/x-pack/plugins/maps/public/index.js b/x-pack/plugins/maps/public/index.js index e77a4b30a22e3b..532bbd09dbd8dd 100644 --- a/x-pack/plugins/maps/public/index.js +++ b/x-pack/plugins/maps/public/index.js @@ -14,6 +14,7 @@ import 'uiExports/search'; import 'uiExports/embeddableFactories'; import 'ui/agg_types'; +import { uiCapabilities } from 'ui/capabilities'; import chrome from 'ui/chrome'; import routes from 'ui/routes'; import 'ui/kbn_top_nav'; @@ -52,6 +53,7 @@ routes $scope.delete = (ids) => { return gisMapSavedObjectLoader.delete(ids); }; + $scope.readOnly = !uiCapabilities.maps.save; }, resolve: { hasMaps: function (kbnUrl) { diff --git a/x-pack/plugins/maps/public/shared/components/map_listing.js b/x-pack/plugins/maps/public/shared/components/map_listing.js index 2d99eda19132b6..0a4accf030a80a 100644 --- a/x-pack/plugins/maps/public/shared/components/map_listing.js +++ b/x-pack/plugins/maps/public/shared/components/map_listing.js @@ -331,13 +331,18 @@ export class MapListing extends React.Component { totalItemCount: this.state.items.length, pageSizeOptions: [10, 20, 50], }; - const selection = { - onSelectionChange: (selection) => { - this.setState({ - selectedIds: selection.map(item => { return item.id; }) - }); - } - }; + + let selection = false; + if (!this.props.readOnly) { + selection = { + onSelectionChange: (selection) => { + this.setState({ + selectedIds: selection.map(item => { return item.id; }) + }); + } + }; + } + const sorting = {}; if (this.state.sortField) { sorting.sort = { @@ -364,7 +369,7 @@ export class MapListing extends React.Component { renderListing() { let createButton; - if (!this.props.hideWriteControls) { + if (!this.props.readOnly) { createButton = ( { xpackMainPlugin.info.feature(thisPlugin.id).registerLicenseCheckResultsGenerator(checkLicense); }); + xpackMainPlugin.registerFeature({ + id: 'ml', + name: i18n.translate('xpack.ml.featureRegistry.mlFeatureName', { + defaultMessage: 'Machine Learning', + }), + icon: 'machineLearningApp', + navLinkId: 'ml', + app: ['ml', 'kibana'], + catalogue: ['ml'], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: ['config'] + }, + ui: [], + }, + description: i18n.translate('xpack.ml.feature.reserved.description', { + defaultMessage: 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.' + }) + } + }); + // Add server routes and initialize the plugin here const commonRouteConfig = { pre: [ diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index ce94e47203b2ad..76fd0675fb415a 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -406,7 +406,7 @@ export class JobsListView extends Component { -
+
diff --git a/x-pack/plugins/monitoring/init.js b/x-pack/plugins/monitoring/init.js index b85a402af5ed84..358fd715d7df6c 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'; @@ -54,6 +55,30 @@ export const init = (monitoringPlugin, server) => { } }); + xpackMainPlugin.registerFeature({ + id: 'monitoring', + name: i18n.translate('xpack.monitoring.featureRegistry.monitoringFeatureName', { + defaultMessage: 'Stack Monitoring', + }), + icon: 'monitoringApp', + navLinkId: 'monitoring', + app: ['monitoring', 'kibana'], + catalogue: ['monitoring'], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: ['config'] + }, + ui: [], + }, + description: i18n.translate('xpack.monitoring.feature.reserved.description', { + defaultMessage: 'To grant users access, you should also assign the monitoring_user role.' + }) + } + }); + const bulkUploader = initBulkUploader(kbnServer, server); const kibanaCollectionEnabled = config.get('xpack.monitoring.kibana.collection.enabled'); const { info: xpackMainInfo } = xpackMainPlugin; diff --git a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js index dd3044dc491bd6..4f1ddb4a9a813f 100644 --- a/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js +++ b/x-pack/plugins/remote_clusters/__jest__/client_integration/remote_clusters_list.test.js @@ -14,6 +14,16 @@ import { getRemoteClusterMock } from '../../fixtures/remote_cluster'; jest.mock('ui/chrome', () => ({ addBasePath: (path) => path || '/api/remote_clusters', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + } })); const testBedOptions = { diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js index 1bf2640590cd34..42e2d6224bdc64 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_date_histogram.test.js @@ -16,6 +16,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js index 132a2faf8b9661..e9b8ea94993a68 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_histogram.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js index 1331bd15ffe219..f125ffb0b8ad69 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_logistics.test.js @@ -17,6 +17,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js index 30923cd8ab73eb..67beb861ea927a 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_metrics.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js index 4810a2dcae20fa..c6629640e1996d 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_review.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: (path) => path, breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js index ef16864bece9ca..cad80f8eef3bf2 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_create_terms.test.js @@ -15,6 +15,7 @@ jest.mock('ui/index_patterns', () => { jest.mock('ui/chrome', () => ({ addBasePath: () => '/api/rollup', breadcrumbs: { set: () => {} }, + getInjected: () => ({}), })); jest.mock('lodash/function/debounce', () => fn => fn); diff --git a/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js b/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js index ec7977ff58fa8c..5af5be2997b4d2 100644 --- a/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js +++ b/x-pack/plugins/rollup/__jest__/client_integration/job_list.test.js @@ -18,6 +18,16 @@ setHttp(axios.create()); jest.mock('ui/chrome', () => ({ addBasePath: (path) => path ? path : 'api/rollup', breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + throw new Error(`Unexpected call to chrome.getInjected with key ${key}`); + } })); jest.mock('../../public/crud_app/services', () => { diff --git a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js index 8b220027a75fb0..c883b8065cd0aa 100644 --- a/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js +++ b/x-pack/plugins/rollup/public/crud_app/sections/job_list/job_list.test.js @@ -11,6 +11,15 @@ import { JobList } from './job_list'; jest.mock('ui/chrome', () => ({ addBasePath: () => {}, breadcrumbs: { set: () => {} }, + getInjected: (key) => { + if (key === 'uiCapabilities') { + return { + navLinks: {}, + management: {}, + catalogue: {} + }; + } + } })); jest.mock('../../services', () => { diff --git a/x-pack/plugins/searchprofiler/public/app.js b/x-pack/plugins/searchprofiler/public/app.js index e6510a1b28cb29..442aa3d7070d69 100644 --- a/x-pack/plugins/searchprofiler/public/app.js +++ b/x-pack/plugins/searchprofiler/public/app.js @@ -8,6 +8,7 @@ // K5 imports import { uiModules } from 'ui/modules'; import uiRoutes from 'ui/routes'; +import 'ui/capabilities/route_setup'; import { notify } from 'ui/notify'; // License @@ -31,6 +32,7 @@ import { defaultQuery } from './templates/default_query'; uiRoutes.when('/dev_tools/searchprofiler', { template: template, + requireUICapability: 'dev_tools.show', controller: ($scope, i18n) => { $scope.registerLicenseLinkLabel = i18n('xpack.searchProfiler.registerLicenseLinkLabel', { defaultMessage: 'register a license' }); diff --git a/x-pack/plugins/searchprofiler/public/templates/index.html b/x-pack/plugins/searchprofiler/public/templates/index.html index 3b1f5d771934ba..9f732121c0a511 100644 --- a/x-pack/plugins/searchprofiler/public/templates/index.html +++ b/x-pack/plugins/searchprofiler/public/templates/index.html @@ -1,4 +1,4 @@ - +
diff --git a/x-pack/plugins/security/common/constants.js b/x-pack/plugins/security/common/constants.ts similarity index 77% rename from x-pack/plugins/security/common/constants.js rename to x-pack/plugins/security/common/constants.ts index 5fb316b7725080..bca0684209ba11 100644 --- a/x-pack/plugins/security/common/constants.js +++ b/x-pack/plugins/security/common/constants.ts @@ -7,3 +7,5 @@ export const GLOBAL_RESOURCE = '*'; export const IGNORED_TYPES = ['space']; export const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; +export const APPLICATION_PREFIX = 'kibana-'; +export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; diff --git a/x-pack/plugins/security/common/model/index_privilege.ts b/x-pack/plugins/security/common/model/features_privileges.ts similarity index 63% rename from x-pack/plugins/security/common/model/index_privilege.ts rename to x-pack/plugins/security/common/model/features_privileges.ts index 560e8df5e126b2..09ed7d7934be35 100644 --- a/x-pack/plugins/security/common/model/index_privilege.ts +++ b/x-pack/plugins/security/common/model/features_privileges.ts @@ -4,11 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface IndexPrivilege { - names: string[]; - privileges: string[]; - field_security?: { - grant?: string[]; - }; - query?: string; +export interface FeaturesPrivileges { + [featureId: string]: string[]; } diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts new file mode 100644 index 00000000000000..17def6cd4c1a8a --- /dev/null +++ b/x-pack/plugins/security/common/model/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { FeaturesPrivileges } from './features_privileges'; +export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; +export { KibanaPrivileges } from './kibana_privileges'; diff --git a/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts new file mode 100644 index 00000000000000..fd4cdf33028ebe --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts @@ -0,0 +1,36 @@ +/* + * 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 { FeaturesPrivileges } from '../features_privileges'; +import { RawKibanaFeaturePrivileges } from '../raw_kibana_privileges'; + +export class KibanaFeaturePrivileges { + constructor(private readonly featurePrivilegesMap: RawKibanaFeaturePrivileges) {} + + public getAllPrivileges(): FeaturesPrivileges { + return Object.entries(this.featurePrivilegesMap).reduce((acc, [featureId, privileges]) => { + return { + ...acc, + [featureId]: Object.keys(privileges), + }; + }, {}); + } + + public getPrivileges(featureId: string): string[] { + const featurePrivileges = this.featurePrivilegesMap[featureId]; + if (featurePrivileges == null) { + return []; + } + + return Object.keys(featurePrivileges); + } + + public getActions(featureId: string, privilege: string): string[] { + if (!this.featurePrivilegesMap[featureId]) { + return []; + } + return this.featurePrivilegesMap[featureId][privilege] || []; + } +} diff --git a/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts new file mode 100644 index 00000000000000..ffe55b813217fb --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/global_privileges.ts @@ -0,0 +1,17 @@ +/* + * 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 class KibanaGlobalPrivileges { + constructor(private readonly globalPrivilegesMap: Record) {} + + public getAllPrivileges(): string[] { + return Object.keys(this.globalPrivilegesMap); + } + + public getActions(privilege: string): string[] { + return this.globalPrivilegesMap[privilege] || []; + } +} diff --git a/x-pack/plugins/security/common/model/kibana_privileges/index.ts b/x-pack/plugins/security/common/model/kibana_privileges/index.ts new file mode 100644 index 00000000000000..ab9baa1356c4b3 --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { KibanaPrivileges } from './kibana_privileges'; diff --git a/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts new file mode 100644 index 00000000000000..61e5f083a7798a --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/kibana_privileges.ts @@ -0,0 +1,26 @@ +/* + * 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 { RawKibanaPrivileges } from '../raw_kibana_privileges'; +import { KibanaFeaturePrivileges } from './feature_privileges'; +import { KibanaGlobalPrivileges } from './global_privileges'; +import { KibanaSpacesPrivileges } from './spaces_privileges'; + +export class KibanaPrivileges { + constructor(private readonly rawKibanaPrivileges: RawKibanaPrivileges) {} + + public getGlobalPrivileges() { + return new KibanaGlobalPrivileges(this.rawKibanaPrivileges.global); + } + + public getSpacesPrivileges() { + return new KibanaSpacesPrivileges(this.rawKibanaPrivileges.space); + } + + public getFeaturePrivileges() { + return new KibanaFeaturePrivileges(this.rawKibanaPrivileges.features); + } +} diff --git a/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts new file mode 100644 index 00000000000000..5c8b4196a2b556 --- /dev/null +++ b/x-pack/plugins/security/common/model/kibana_privileges/spaces_privileges.ts @@ -0,0 +1,17 @@ +/* + * 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 class KibanaSpacesPrivileges { + constructor(private readonly spacesPrivilegesMap: Record) {} + + public getAllPrivileges(): string[] { + return Object.keys(this.spacesPrivilegesMap); + } + + public getActions(privilege: string): string[] { + return this.spacesPrivilegesMap[privilege] || []; + } +} diff --git a/x-pack/plugins/security/common/model/raw_kibana_privileges.ts b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts new file mode 100644 index 00000000000000..1b1584a4ce58c0 --- /dev/null +++ b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts @@ -0,0 +1,18 @@ +/* + * 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 RawKibanaFeaturePrivileges { + [featureId: string]: { + [privilegeId: string]: string[]; + }; +} + +export interface RawKibanaPrivileges { + global: Record; + features: RawKibanaFeaturePrivileges; + space: Record; + reserved: Record; +} diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 5b1094c8c3a0ab..19bea7ccdfefb3 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -4,26 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IndexPrivilege } from './index_privilege'; -import { KibanaPrivilege } from './kibana_privilege'; +import { FeaturesPrivileges } from './features_privileges'; + +export interface RoleIndexPrivilege { + names: string[]; + privileges: string[]; + field_security?: { + grant?: string[]; + }; + query?: string; +} + +export interface RoleKibanaPrivilege { + spaces: string[]; + base: string[]; + feature: FeaturesPrivileges; + _reserved?: string[]; +} export interface Role { name: string; elasticsearch: { cluster: string[]; - indices: IndexPrivilege[]; + indices: RoleIndexPrivilege[]; run_as: string[]; }; - kibana: { - global: KibanaPrivilege[]; - space: { - [spaceId: string]: KibanaPrivilege[]; - }; - }; + kibana: RoleKibanaPrivilege[]; metadata?: { [anyKey: string]: any; }; transient_metadata?: { [anyKey: string]: any; }; + _transform_error?: string[]; + _unrecognized_applications?: string[]; } diff --git a/x-pack/plugins/security/common/privilege_calculator_utils.test.ts b/x-pack/plugins/security/common/privilege_calculator_utils.test.ts new file mode 100644 index 00000000000000..7a70750656f1db --- /dev/null +++ b/x-pack/plugins/security/common/privilege_calculator_utils.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { areActionsFullyCovered, compareActions } from './privilege_calculator_utils'; + +describe('#compareActions', () => { + it(`returns -1 when the first action set is more permissive than the second action set`, () => { + const actionSet1 = ['foo:/*', 'bar']; + const actionSet2 = ['foo:/*']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it(`returns 1 when the second action set is more permissive than the first action set`, () => { + const actionSet1 = ['foo:/*']; + const actionSet2 = ['foo:/*', 'bar']; + expect(compareActions(actionSet1, actionSet2)).toEqual(1); + }); + + it('works without wildcards', () => { + const actionSet1 = ['foo:/bar', 'foo:/bar/baz', 'login', 'somethingElse']; + const actionSet2 = ['foo:/bar', 'foo:/bar/baz', 'login']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it('handles wildcards correctly', () => { + const actionSet1 = ['foo:/bar/*']; + const actionSet2 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it('supports ties in a stable-sort order', () => { + const actionSet1 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + const actionSet2 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + expect(compareActions(actionSet1, actionSet2)).toEqual(-1); + }); + + it('does not support actions where one is not a subset of the other', () => { + const actionSet1 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + const actionSet2 = ['bar:/*']; + + // check both directions + expect(() => compareActions(actionSet1, actionSet2)).toThrowErrorMatchingInlineSnapshot( + `"Non-comparable action sets! Expected one set of actions to be a subset of the other!"` + ); + expect(() => compareActions(actionSet2, actionSet1)).toThrowErrorMatchingInlineSnapshot( + `"Non-comparable action sets! Expected one set of actions to be a subset of the other!"` + ); + }); +}); + +describe('#areActionsFullyCovered', () => { + it('returns true for two empty sets', () => { + const actionSet1: string[] = []; + const actionSet2: string[] = []; + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(true); + }); + + it('returns true when the first set fully covers the second set', () => { + const actionSet1: string[] = ['foo:/*', 'bar:/*']; + const actionSet2: string[] = ['foo:/bar', 'bar:/baz']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(true); + }); + + it('returns false when the first set does not fully cover the second set', () => { + const actionSet1: string[] = ['foo:/bar', 'bar:/baz']; + const actionSet2: string[] = ['foo:/*', 'bar:/*']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(false); + }); + + it('returns true for ties', () => { + const actionSet1: string[] = ['foo:/bar', 'bar:/baz']; + const actionSet2: string[] = ['foo:/bar', 'bar:/baz']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(true); + }); + + it('can handle actions where one is not a subset of the other', () => { + const actionSet1 = ['foo:/bar/bam', 'foo:/bar/baz/*']; + const actionSet2 = ['bar:/*']; + + expect(areActionsFullyCovered(actionSet1, actionSet2)).toEqual(false); + }); +}); diff --git a/x-pack/plugins/security/common/privilege_calculator_utils.ts b/x-pack/plugins/security/common/privilege_calculator_utils.ts new file mode 100644 index 00000000000000..e767862c46757b --- /dev/null +++ b/x-pack/plugins/security/common/privilege_calculator_utils.ts @@ -0,0 +1,58 @@ +/* + * 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'; + +/** + * Given two sets of actions, where one set is known to be a subset of the other, this will + * determine which set of actions is most permissive, using standard sorting return values: + * -1: actions1 is most permissive + * 1: actions2 is most permissive + * + * All privileges are hierarchal at this point. + * + * @param actionSet1 + * @param actionSet2 + */ +export function compareActions(actionSet1: string[], actionSet2: string[]) { + if (areActionsFullyCovered(actionSet1, actionSet2)) { + return -1; + } + if (areActionsFullyCovered(actionSet2, actionSet1)) { + return 1; + } + throw new Error( + `Non-comparable action sets! Expected one set of actions to be a subset of the other!` + ); +} +/** + * Given two sets of actions, this will determine if the first set fully covers the second set. + * "fully covers" means that all of the actions granted by the second set are also granted by the first set. + * @param actionSet1 + * @param actionSet2 + */ +export function areActionsFullyCovered(actionSet1: string[], actionSet2: string[]) { + const actionExpressions = actionSet1.map(actionToRegExp); + + const isFullyCovered = actionSet2.every((assigned: string) => + // Does any expression from the first set match this action in the second set? + actionExpressions.some((exp: RegExp) => exp.test(assigned)) + ); + + return isFullyCovered; +} + +function actionToRegExp(action: string) { + // Actions are strings that may or may not end with a wildcard ("*"). + // This will excape all characters in the action string that are not the wildcard character. + // Each wildcard character is then turned into a ".*" before the entire thing is turned into a regexp. + return new RegExp( + action + .split('*') + .map(part => _.escapeRegExp(part)) + .join('.*') + ); +} diff --git a/x-pack/plugins/security/index.js b/x-pack/plugins/security/index.js index 42f8d707922c24..c4589cb6b8c6d1 100644 --- a/x-pack/plugins/security/index.js +++ b/x-pack/plugins/security/index.js @@ -9,6 +9,7 @@ import { getUserProvider } from './server/lib/get_user'; import { initAuthenticateApi } from './server/routes/api/v1/authenticate'; import { initUsersApi } from './server/routes/api/v1/users'; import { initPublicRolesApi } from './server/routes/api/public/roles'; +import { initPrivilegesApi } from './server/routes/api/public/privileges'; import { initIndicesApi } from './server/routes/api/v1/indices'; import { initLoginView } from './server/routes/views/login'; import { initLogoutView } from './server/routes/views/logout'; @@ -19,10 +20,18 @@ 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, + initAPIAuthorization, + initAppAuthorization, + registerPrivilegesWithCluster, + validateFeaturePrivileges +} 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', @@ -93,9 +102,45 @@ 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) { + // if we have a license which doesn't enable security, or we're a legacy user + // we shouldn't disable any ui capabilities + const { authorization } = server.plugins.security; + if (!authorization.mode.useRbacForRequest(request)) { + return originalInjectedVars; + } + + 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) + }; } }, + async postInit(server) { + const plugin = this; + + const xpackMainPlugin = server.plugins.xpack_main; + + watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { + if (license.allowRbac) { + const { security } = server.plugins; + await validateFeaturePrivileges(security.authorization.actions, xpackMainPlugin.getFeatures()); + await registerPrivilegesWithCluster(server); + } + }); + }, + async init(server) { const plugin = this; @@ -120,19 +165,16 @@ export const security = (kibana) => new kibana.Plugin({ // automatically assigned to all routes that don't contain an auth config. server.auth.default('session'); + const { savedObjects } = server; + + const spaces = createOptionalPlugin(config, 'xpack.spaces', server.plugins, 'spaces'); + // exposes server.plugins.security.authorization - const authorization = createAuthorizationService(server, xpackInfoFeature); + const authorization = createAuthorizationService(server, xpackInfoFeature, xpackMainPlugin, spaces); server.expose('authorization', deepFreeze(authorization)); - watchStatusAndLicenseToInitialize(xpackMainPlugin, plugin, async (license) => { - if (license.allowRbac) { - await registerPrivilegesWithCluster(server); - } - }); - const auditLogger = new SecurityAuditLogger(server.config(), new AuditLogger(server, 'security')); - const { savedObjects } = server; savedObjects.setScopedSavedObjectsClientFactory(({ request, }) => { @@ -151,17 +193,14 @@ 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, }); } @@ -172,9 +211,12 @@ export const security = (kibana) => new kibana.Plugin({ await initAuthenticator(server); initAuthenticateApi(server); + initAPIAuthorization(server, authorization); + initAppAuthorization(server, xpackMainPlugin, authorization); initUsersApi(server); initPublicRolesApi(server); initIndicesApi(server); + initPrivilegesApi(server); initLoginView(server, xpackMainPlugin); initLogoutView(server); initLoggedOutView(server); diff --git a/x-pack/plugins/security/public/components/_index.scss b/x-pack/plugins/security/public/components/_index.scss new file mode 100644 index 00000000000000..707dc73de00f2c --- /dev/null +++ b/x-pack/plugins/security/public/components/_index.scss @@ -0,0 +1 @@ +@import './management/users/index'; diff --git a/x-pack/plugins/security/public/components/management/users/_index.scss b/x-pack/plugins/security/public/components/management/users/_index.scss new file mode 100644 index 00000000000000..c5da74aa3f785c --- /dev/null +++ b/x-pack/plugins/security/public/components/management/users/_index.scss @@ -0,0 +1 @@ +@import './users'; diff --git a/x-pack/plugins/security/public/components/management/users/_users.scss b/x-pack/plugins/security/public/components/management/users/_users.scss new file mode 100644 index 00000000000000..d06ec3dd526f1d --- /dev/null +++ b/x-pack/plugins/security/public/components/management/users/_users.scss @@ -0,0 +1,16 @@ +// HACK -- Fix for background color full-height of browser +.secUsersEditPage, +.secUsersListingPage { + min-height: calc(100vh - 70px); +} + +.secUsersListingPage__content { + flex-grow: 0; +} + +.secUsersEditPage__content { + max-width: $secFormWidth; + margin-left: auto; + margin-right: auto; + flex-grow: 0; +} diff --git a/x-pack/plugins/security/public/components/management/users/edit_user.js b/x-pack/plugins/security/public/components/management/users/edit_user.js index cfb05f83cfa289..d3ce0d4da62477 100644 --- a/x-pack/plugins/security/public/components/management/users/edit_user.js +++ b/x-pack/plugins/security/public/components/management/users/edit_user.js @@ -384,8 +384,8 @@ class EditUserUI extends Component { } return ( -
- +
+ diff --git a/x-pack/plugins/security/public/components/management/users/users.js b/x-pack/plugins/security/public/components/management/users/users.js index 4907fbfc1c1972..8a526c70355a7b 100644 --- a/x-pack/plugins/security/public/components/management/users/users.js +++ b/x-pack/plugins/security/public/components/management/users/users.js @@ -91,7 +91,7 @@ class UsersUI extends Component { const { apiClient, intl } = this.props; if (permissionDenied) { return ( -
+
- +
+ diff --git a/x-pack/plugins/security/public/index.scss b/x-pack/plugins/security/public/index.scss index e87a2f5c95287a..2d7696bed39890 100644 --- a/x-pack/plugins/security/public/index.scss +++ b/x-pack/plugins/security/public/index.scss @@ -1,10 +1,17 @@ @import 'src/legacy/ui/public/styles/styling_constants'; -// Logged out styles -@import './views/logged_out/index'; +// Prefix all styles with "kbn" to avoid conflicts. +// Examples +// secChart +// secChart__legend +// secChart__legend--small +// secChart__legend-isLoading -// Login styles -@import './views/login/index'; +$secFormWidth: 460px; + +// Public components +@import './components/index'; + +// Public views +@import './views/index'; -// Management styles -@import './views/management/index'; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.ts new file mode 100644 index 00000000000000..cbf3b2a384f713 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/build_role.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 { FeaturesPrivileges, Role } from '../../../../common/model'; + +export interface BuildRoleOpts { + spacesPrivileges?: Array<{ + spaces: string[]; + base: string[]; + feature: FeaturesPrivileges; + }>; +} + +export const buildRole = (options: BuildRoleOpts = {}) => { + const role: Role = { + name: 'unit test role', + elasticsearch: { + indices: [], + cluster: [], + run_as: [], + }, + kibana: [], + }; + + if (options.spacesPrivileges) { + role.kibana.push(...options.spacesPrivileges); + } else { + role.kibana.push({ + spaces: [], + base: [], + feature: {}, + }); + } + + return role; +}; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts new file mode 100644 index 00000000000000..3c2582475089bb --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/common_allowed_privileges.ts @@ -0,0 +1,52 @@ +/* + * 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 const unrestrictedBasePrivileges = { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, +}; +export const unrestrictedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature3: { + privileges: ['all'], + canUnassign: true, + }, + }, +}; + +export const fullyRestrictedBasePrivileges = { + base: { + privileges: ['all'], + canUnassign: false, + }, +}; + +export const fullyRestrictedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: false, + }, + }, +}; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts new file mode 100644 index 00000000000000..d598a9da67a51d --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.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 { KibanaPrivileges } from '../../../../common/model'; + +export const defaultPrivilegeDefinition = new KibanaPrivileges({ + global: { + all: ['api:/*', 'ui:/*'], + read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/*'], + }, + space: { + all: [ + 'api:/feature1/*', + 'ui:/feature1/*', + 'api:/feature2/*', + 'ui:/feature2/*', + 'ui:/feature3/foo', + 'ui:/feature3/foo/*', + ], + read: ['ui:/feature1/foo', 'ui:/feature2/foo', 'ui:/feature3/foo/bar'], + }, + features: { + feature1: { + all: ['ui:/feature1/foo', 'ui:/feature1/bar'], + read: ['ui:/feature1/foo'], + }, + feature2: { + all: ['ui:/feature2/foo', 'api:/feature2/bar'], + read: ['ui:/feature2/foo'], + }, + feature3: { + all: ['ui:/feature3/foo', 'ui:/feature3/foo/*'], + }, + }, + reserved: {}, +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts new file mode 100644 index 00000000000000..253dcaed9f19e0 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { defaultPrivilegeDefinition } from './default_privilege_definition'; +export { buildRole, BuildRoleOpts } from './build_role'; +export * from './common_allowed_privileges'; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts new file mode 100644 index 00000000000000..056a4d3022fc5c --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory'; +export * from './kibana_privilege_calculator_types'; diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts new file mode 100644 index 00000000000000..ac3c74a987766d --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.test.ts @@ -0,0 +1,296 @@ +/* + * 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 { KibanaPrivileges, Role } from '../../../common/model'; +import { + buildRole, + defaultPrivilegeDefinition, + fullyRestrictedBasePrivileges, + fullyRestrictedFeaturePrivileges, + unrestrictedBasePrivileges, + unrestrictedFeaturePrivileges, +} from './__fixtures__'; +import { KibanaAllowedPrivilegesCalculator } from './kibana_allowed_privileges_calculator'; +import { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory'; + +const buildAllowedPrivilegesCalculator = ( + role: Role, + kibanaPrivilege: KibanaPrivileges = defaultPrivilegeDefinition +) => { + return new KibanaAllowedPrivilegesCalculator(kibanaPrivilege, role); +}; + +const buildEffectivePrivilegesCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const factory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges); + return factory.getInstance(role); +}; + +describe('AllowedPrivileges', () => { + it('allows all privileges when none are currently assigned', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + ]); + }); + + it('allows all global base privileges, but just "all" for everything else when global is set to "all"', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + { + ...fullyRestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when global base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + const expectedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by global "all" + }, + }, + }; + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...expectedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: false, + }, + ...expectedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when space base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); + + it(`allows space base privilege to be set to "all" or "read" when space base is already "all"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: false, + }, + }, + }, + ]); + }); + + it(`restricts space feature privileges when global feature privileges are set`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + feature2: ['read'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivilegesCalculator(role); + const allowedPrivilegesCalculator = buildAllowedPrivilegesCalculator(role); + + const result = allowedPrivilegesCalculator.calculateAllowedPrivileges( + effectivePrivileges.calculateEffectivePrivileges(true) + ); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts new file mode 100644 index 00000000000000..f2e2c4bc1be990 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts @@ -0,0 +1,153 @@ +/* + * 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; +import { areActionsFullyCovered, compareActions } from '../../../common/privilege_calculator_utils'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { + AllowedPrivilege, + CalculatedPrivilege, + PRIVILEGE_SOURCE, +} from './kibana_privilege_calculator_types'; + +export class KibanaAllowedPrivilegesCalculator { + // reference to the global privilege definition + private globalPrivilege: RoleKibanaPrivilege; + + // list of privilege actions that comprise the global base privilege + private assignedGlobalBaseActions: string[]; + + constructor(private readonly kibanaPrivileges: KibanaPrivileges, private readonly role: Role) { + this.globalPrivilege = this.locateGlobalPrivilege(role); + this.assignedGlobalBaseActions = this.globalPrivilege.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(this.globalPrivilege.base[0]) + : []; + } + + public calculateAllowedPrivileges( + effectivePrivileges: CalculatedPrivilege[] + ): AllowedPrivilege[] { + const { kibana = [] } = this.role; + return kibana.map((privilegeSpec, index) => + this.calculateAllowedPrivilege(privilegeSpec, effectivePrivileges[index]) + ); + } + + private calculateAllowedPrivilege( + privilegeSpec: RoleKibanaPrivilege, + effectivePrivileges: CalculatedPrivilege + ): AllowedPrivilege { + const result: AllowedPrivilege = { + base: { + privileges: [], + canUnassign: true, + }, + feature: {}, + }; + + if (isGlobalPrivilegeDefinition(privilegeSpec)) { + // nothing can impede global privileges + result.base.canUnassign = true; + result.base.privileges = this.kibanaPrivileges.getGlobalPrivileges().getAllPrivileges(); + } else { + // space base privileges are restricted based on the assigned global privileges + const spacePrivileges = this.kibanaPrivileges.getSpacesPrivileges().getAllPrivileges(); + result.base.canUnassign = this.assignedGlobalBaseActions.length === 0; + result.base.privileges = spacePrivileges.filter(privilege => { + // always allowed to assign the calculated effective privilege + if (privilege === effectivePrivileges.base.actualPrivilege) { + return true; + } + + const privilegeActions = this.getBaseActions(PRIVILEGE_SOURCE.SPACE_BASE, privilege); + return !areActionsFullyCovered(this.assignedGlobalBaseActions, privilegeActions); + }); + } + + const allFeaturePrivileges = this.kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + result.feature = Object.entries(allFeaturePrivileges).reduce( + (acc, [featureId, featurePrivileges]) => { + return { + ...acc, + [featureId]: this.getAllowedFeaturePrivileges( + effectivePrivileges, + featureId, + featurePrivileges + ), + }; + }, + {} + ); + + return result; + } + + private getAllowedFeaturePrivileges( + effectivePrivileges: CalculatedPrivilege, + featureId: string, + candidateFeaturePrivileges: string[] + ): { privileges: string[]; canUnassign: boolean } { + const effectiveFeaturePrivilegeExplanation = effectivePrivileges.feature[featureId]; + if (effectiveFeaturePrivilegeExplanation == null) { + throw new Error('To calculate allowed feature privileges, we need the effective privileges'); + } + + const effectiveFeatureActions = this.getFeatureActions( + featureId, + effectiveFeaturePrivilegeExplanation.actualPrivilege + ); + + const privileges = []; + if (effectiveFeaturePrivilegeExplanation.actualPrivilege !== NO_PRIVILEGE_VALUE) { + // Always allowed to assign the calculated effective privilege + privileges.push(effectiveFeaturePrivilegeExplanation.actualPrivilege); + } + + privileges.push( + ...candidateFeaturePrivileges.filter(privilegeId => { + const candidateActions = this.getFeatureActions(featureId, privilegeId); + return compareActions(effectiveFeatureActions, candidateActions) > 0; + }) + ); + + const result = { + privileges: privileges.sort(), + canUnassign: effectiveFeaturePrivilegeExplanation.actualPrivilege === NO_PRIVILEGE_VALUE, + }; + + return result; + } + + private getBaseActions(source: PRIVILEGE_SOURCE, privilegeId: string) { + switch (source) { + case PRIVILEGE_SOURCE.GLOBAL_BASE: + return this.assignedGlobalBaseActions; + case PRIVILEGE_SOURCE.SPACE_BASE: + return this.kibanaPrivileges.getSpacesPrivileges().getActions(privilegeId); + default: + throw new Error( + `Cannot get base actions for unsupported privilege source ${PRIVILEGE_SOURCE[source]}` + ); + } + } + + private getFeatureActions(featureId: string, privilegeId: string): string[] { + return this.kibanaPrivileges.getFeaturePrivileges().getActions(featureId, privilegeId); + } + + private locateGlobalPrivilege(role: Role) { + const spacePrivileges = role.kibana; + return ( + spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)) || { + spaces: [] as string[], + base: [] as string[], + feature: {}, + } + ); + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts new file mode 100644 index 00000000000000..d5c2727b35b139 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.test.ts @@ -0,0 +1,321 @@ +/* + * 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { buildRole, defaultPrivilegeDefinition } from './__fixtures__'; +import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types'; + +const buildEffectiveBasePrivilegeCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const globalPrivilegeSpec = + role.kibana.find(k => isGlobalPrivilegeDefinition(k)) || + ({ + spaces: ['*'], + base: [], + feature: {}, + } as RoleKibanaPrivilege); + + const globalActions = globalPrivilegeSpec.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0]) + : []; + + return new KibanaBasePrivilegeCalculator(kibanaPrivileges, globalPrivilegeSpec, globalActions); +}; + +describe('getMostPermissiveBasePrivilege', () => { + describe('without ignoring assigned', () => { + it('returns "none" when no privileges are granted', () => { + const role = buildRole(); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + false + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + + defaultPrivilegeDefinition + .getGlobalPrivileges() + .getAllPrivileges() + .forEach(globalBasePrivilege => { + it(`returns "${globalBasePrivilege}" when assigned directly at the global privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [globalBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + false + ); + + expect(result).toEqual({ + actualPrivilege: globalBasePrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + defaultPrivilegeDefinition + .getSpacesPrivileges() + .getAllPrivileges() + .forEach(spaceBasePrivilege => { + it(`returns "${spaceBasePrivilege}" when assigned directly at the space base privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: [spaceBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + false + ); + + expect(result).toEqual({ + actualPrivilege: spaceBasePrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + it('returns the global privilege when no space base is defined', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + false + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + + it('returns the global privilege when it supercedes the space privilege', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + false + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + } as PrivilegeExplanation); + }); + }); + + describe('ignoring assigned', () => { + it('returns "none" when no privileges are granted', () => { + const role = buildRole(); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + true + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + + defaultPrivilegeDefinition + .getGlobalPrivileges() + .getAllPrivileges() + .forEach(globalBasePrivilege => { + it(`returns "none" when "${globalBasePrivilege}" assigned directly at the global privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [globalBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + true + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + defaultPrivilegeDefinition + .getSpacesPrivileges() + .getAllPrivileges() + .forEach(spaceBasePrivilege => { + it(`returns "none" when "${spaceBasePrivilege}" when assigned directly at the space base privilege`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: [spaceBasePrivilege], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[0], + true + ); + + expect(result).toEqual({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + } as PrivilegeExplanation); + }); + }); + + it('returns the global privilege when no space base is defined', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + true + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + + it('returns the global privilege when it supercedes the space privilege, without indicating override', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + true + ); + + expect(result).toEqual({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + + it('returns the global privilege even though it would ordinarly be overriden by space base privilege', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectiveBasePrivilegesCalculator = buildEffectiveBasePrivilegeCalculator(role); + const result: PrivilegeExplanation = effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + role.kibana[1], + true + ); + + expect(result).toEqual({ + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + } as PrivilegeExplanation); + }); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts new file mode 100644 index 00000000000000..37ed5b6c02e9b4 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_base_privilege_calculator.ts @@ -0,0 +1,99 @@ +/* + * 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 { KibanaPrivileges, RoleKibanaPrivilege } from '../../../common/model'; +import { compareActions } from '../../../common/privilege_calculator_utils'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from './kibana_privilege_calculator_types'; + +export class KibanaBasePrivilegeCalculator { + constructor( + private readonly kibanaPrivileges: KibanaPrivileges, + private readonly globalPrivilege: RoleKibanaPrivilege, + private readonly assignedGlobalBaseActions: string[] + ) {} + + public getMostPermissiveBasePrivilege( + privilegeSpec: RoleKibanaPrivilege, + ignoreAssigned: boolean + ): PrivilegeExplanation { + const assignedPrivilege = privilegeSpec.base[0] || NO_PRIVILEGE_VALUE; + + // If this is the global privilege definition, then there is nothing to supercede it. + if (isGlobalPrivilegeDefinition(privilegeSpec)) { + if (assignedPrivilege === NO_PRIVILEGE_VALUE || ignoreAssigned) { + return { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }; + } + return { + actualPrivilege: assignedPrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }; + } + + // Otherwise, check to see if the global privilege supercedes this one. + const baseActions = [ + ...this.kibanaPrivileges.getSpacesPrivileges().getActions(assignedPrivilege), + ]; + + const globalSupercedes = + this.hasAssignedGlobalBasePrivilege() && + (compareActions(this.assignedGlobalBaseActions, baseActions) < 0 || ignoreAssigned); + + if (globalSupercedes) { + const wasDirectlyAssigned = !ignoreAssigned && baseActions.length > 0; + + return { + actualPrivilege: this.globalPrivilege.base[0], + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + ...this.buildSupercededFields( + wasDirectlyAssigned, + assignedPrivilege, + PRIVILEGE_SOURCE.SPACE_BASE + ), + }; + } + + if (!ignoreAssigned) { + return { + actualPrivilege: assignedPrivilege, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }; + } + + return { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }; + } + + private hasAssignedGlobalBasePrivilege() { + return this.assignedGlobalBaseActions.length > 0; + } + + private buildSupercededFields( + isSuperceding: boolean, + supersededPrivilege?: string, + supersededPrivilegeSource?: PRIVILEGE_SOURCE + ) { + if (!isSuperceding) { + return {}; + } + return { + supersededPrivilege, + supersededPrivilegeSource, + }; + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts new file mode 100644 index 00000000000000..d21255dc1c5152 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.test.ts @@ -0,0 +1,868 @@ +/* + * 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 { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../common/model'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { buildRole, BuildRoleOpts, defaultPrivilegeDefinition } from './__fixtures__'; +import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator'; +import { KibanaFeaturePrivilegeCalculator } from './kibana_feature_privilege_calculator'; +import { PRIVILEGE_SOURCE } from './kibana_privilege_calculator_types'; + +const buildEffectiveBasePrivilegeCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const globalPrivilegeSpec = + role.kibana.find(k => isGlobalPrivilegeDefinition(k)) || + ({ + spaces: ['*'], + base: [], + feature: {}, + } as RoleKibanaPrivilege); + + const globalActions = globalPrivilegeSpec.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0]) + : []; + + return new KibanaBasePrivilegeCalculator(kibanaPrivileges, globalPrivilegeSpec, globalActions); +}; + +const buildEffectiveFeaturePrivilegeCalculator = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const globalPrivilegeSpec = + role.kibana.find(k => isGlobalPrivilegeDefinition(k)) || + ({ + spaces: ['*'], + base: [], + feature: {}, + } as RoleKibanaPrivilege); + + const globalActions = globalPrivilegeSpec.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilegeSpec.base[0]) + : []; + + const rankedFeaturePrivileges = kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + + return new KibanaFeaturePrivilegeCalculator( + kibanaPrivileges, + globalPrivilegeSpec, + globalActions, + rankedFeaturePrivileges + ); +}; + +interface TestOpts { + only?: boolean; + role?: BuildRoleOpts; + privilegeIndex?: number; + ignoreAssigned?: boolean; + result: Record; +} + +function runTest( + description: string, + { + role: roleOpts = {}, + result = {}, + privilegeIndex = 0, + ignoreAssigned = false, + only = false, + }: TestOpts +) { + const fn = only ? it.only : it; + fn(description, () => { + const role = buildRole(roleOpts); + const basePrivilegeCalculator = buildEffectiveBasePrivilegeCalculator(role); + const featurePrivilegeCalculator = buildEffectiveFeaturePrivilegeCalculator(role); + + const baseExplanation = basePrivilegeCalculator.getMostPermissiveBasePrivilege( + role.kibana[privilegeIndex], + // If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is + // without ignoring assigned, in order to calculate the correct feature privileges. + false + ); + + const actualResult = featurePrivilegeCalculator.getMostPermissiveFeaturePrivilege( + role.kibana[privilegeIndex], + baseExplanation, + 'feature1', + ignoreAssigned + ); + + expect(actualResult).toEqual(result); + }); +} + +describe('getMostPermissiveFeaturePrivilege', () => { + describe('for global feature privileges, without ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + ], + }, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + }, + } + ); + + runTest( + 'returns "all" when assigned as the feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + } + ); + }); + + describe('for global feature privileges, ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "none" when "read" is assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "read" when "all" assigned as the feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + }); + + describe('for space feature privileges, without ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned global feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the space feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + } + ); + + runTest( + 'returns "all" when assigned as the space base privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + } + ); + + runTest('returns "all" when assigned everywhere, without indicating override', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "all" when assigned at global feature, overriding space feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }, + }); + }); + + describe('for space feature privileges, ignoring assigned', () => { + runTest('returns "none" when no privileges are granted', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "none" when "read" assigned directly to the feature', { + role: { + spacesPrivileges: [ + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + ignoreAssigned: true, + result: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }); + + runTest('returns "read" when assigned as the global base privilege', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest( + 'returns "all" when assigned as the global base privilege, which overrides assigned global feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which normally overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global feature privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "read" when "all" assigned as the space feature privilege, which normally overrides assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the space base privilege, which does not override assigned global base privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: {}, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest( + 'returns "all" when assigned as the global base privilege, which normally overrides assigned space feature privilege', + { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + } + ); + + runTest('returns "all" when assigned everywhere, without indicating override', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: ['all'], + feature: { + feature1: ['all'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }, + }); + + runTest('returns "all" when assigned at global feature, normally overriding space feature', { + role: { + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + }, + }, + ], + }, + privilegeIndex: 1, + ignoreAssigned: true, + result: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: false, + }, + }); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts new file mode 100644 index 00000000000000..1cdc70878ecd66 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_feature_privilege_calculator.ts @@ -0,0 +1,198 @@ +/* + * 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 { FeaturesPrivileges, KibanaPrivileges, RoleKibanaPrivilege } from '../../../common/model'; +import { areActionsFullyCovered } from '../../../common/privilege_calculator_utils'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { + PRIVILEGE_SOURCE, + PrivilegeExplanation, + PrivilegeScenario, +} from './kibana_privilege_calculator_types'; + +export class KibanaFeaturePrivilegeCalculator { + constructor( + private readonly kibanaPrivileges: KibanaPrivileges, + private readonly globalPrivilege: RoleKibanaPrivilege, + private readonly assignedGlobalBaseActions: string[], + private readonly rankedFeaturePrivileges: FeaturesPrivileges + ) {} + + public getMostPermissiveFeaturePrivilege( + privilegeSpec: RoleKibanaPrivilege, + basePrivilegeExplanation: PrivilegeExplanation, + featureId: string, + ignoreAssigned: boolean + ): PrivilegeExplanation { + const scenarios = this.buildFeaturePrivilegeScenarios( + privilegeSpec, + basePrivilegeExplanation, + featureId, + ignoreAssigned + ); + + const featurePrivileges = this.rankedFeaturePrivileges[featureId] || []; + + // inspect feature privileges in ranked order (most permissive -> least permissive) + for (const featurePrivilege of featurePrivileges) { + const actions = this.kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, featurePrivilege); + + // check if any of the scenarios satisfy the privilege - first one wins. + for (const scenario of scenarios) { + if (areActionsFullyCovered(scenario.actions, actions)) { + return { + actualPrivilege: featurePrivilege, + actualPrivilegeSource: scenario.actualPrivilegeSource, + isDirectlyAssigned: scenario.isDirectlyAssigned, + ...this.buildSupercededFields( + !scenario.isDirectlyAssigned, + scenario.supersededPrivilege, + scenario.supersededPrivilegeSource + ), + }; + } + } + } + + const isGlobal = isGlobalPrivilegeDefinition(privilegeSpec); + return { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: isGlobal + ? PRIVILEGE_SOURCE.GLOBAL_FEATURE + : PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }; + } + + private buildFeaturePrivilegeScenarios( + privilegeSpec: RoleKibanaPrivilege, + basePrivilegeExplanation: PrivilegeExplanation, + featureId: string, + ignoreAssigned: boolean + ): PrivilegeScenario[] { + const scenarios: PrivilegeScenario[] = []; + + const isGlobalPrivilege = isGlobalPrivilegeDefinition(privilegeSpec); + + const assignedGlobalFeaturePrivilege = this.getAssignedFeaturePrivilege( + this.globalPrivilege, + featureId + ); + + const assignedFeaturePrivilege = this.getAssignedFeaturePrivilege(privilegeSpec, featureId); + const hasAssignedFeaturePrivilege = + !ignoreAssigned && assignedFeaturePrivilege !== NO_PRIVILEGE_VALUE; + + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + actions: [...this.assignedGlobalBaseActions], + ...this.buildSupercededFields( + hasAssignedFeaturePrivilege, + assignedFeaturePrivilege, + isGlobalPrivilege ? PRIVILEGE_SOURCE.GLOBAL_FEATURE : PRIVILEGE_SOURCE.SPACE_FEATURE + ), + }); + + if (!isGlobalPrivilege || !ignoreAssigned) { + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + actions: this.getFeatureActions(featureId, assignedGlobalFeaturePrivilege), + isDirectlyAssigned: isGlobalPrivilege && hasAssignedFeaturePrivilege, + ...this.buildSupercededFields( + hasAssignedFeaturePrivilege && !isGlobalPrivilege, + assignedFeaturePrivilege, + PRIVILEGE_SOURCE.SPACE_FEATURE + ), + }); + } + + if (isGlobalPrivilege) { + return this.rankScenarios(scenarios); + } + + // Otherwise, this is a space feature privilege + + const includeSpaceBaseScenario = + basePrivilegeExplanation.actualPrivilegeSource === PRIVILEGE_SOURCE.SPACE_BASE || + basePrivilegeExplanation.supersededPrivilegeSource === PRIVILEGE_SOURCE.SPACE_BASE; + + const spaceBasePrivilege = + basePrivilegeExplanation.supersededPrivilege || basePrivilegeExplanation.actualPrivilege; + + if (includeSpaceBaseScenario) { + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + actions: this.getBaseActions(PRIVILEGE_SOURCE.SPACE_BASE, spaceBasePrivilege), + ...this.buildSupercededFields( + hasAssignedFeaturePrivilege, + assignedFeaturePrivilege, + PRIVILEGE_SOURCE.SPACE_FEATURE + ), + }); + } + + if (!ignoreAssigned) { + scenarios.push({ + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + actions: this.getFeatureActions( + featureId, + this.getAssignedFeaturePrivilege(privilegeSpec, featureId) + ), + }); + } + + return this.rankScenarios(scenarios); + } + + private rankScenarios(scenarios: PrivilegeScenario[]): PrivilegeScenario[] { + return scenarios.sort( + (scenario1, scenario2) => scenario1.actualPrivilegeSource - scenario2.actualPrivilegeSource + ); + } + + private getBaseActions(source: PRIVILEGE_SOURCE, privilegeId: string) { + switch (source) { + case PRIVILEGE_SOURCE.GLOBAL_BASE: + return this.assignedGlobalBaseActions; + case PRIVILEGE_SOURCE.SPACE_BASE: + return this.kibanaPrivileges.getSpacesPrivileges().getActions(privilegeId); + default: + throw new Error( + `Cannot get base actions for unsupported privilege source ${PRIVILEGE_SOURCE[source]}` + ); + } + } + + private getFeatureActions(featureId: string, privilegeId: string) { + return this.kibanaPrivileges.getFeaturePrivileges().getActions(featureId, privilegeId); + } + + private getAssignedFeaturePrivilege(privilegeSpec: RoleKibanaPrivilege, featureId: string) { + const featureEntry = privilegeSpec.feature[featureId] || []; + return featureEntry[0] || NO_PRIVILEGE_VALUE; + } + + private buildSupercededFields( + isSuperceding: boolean, + supersededPrivilege?: string, + supersededPrivilegeSource?: PRIVILEGE_SOURCE + ) { + if (!isSuperceding) { + return {}; + } + return { + supersededPrivilege, + supersededPrivilegeSource, + }; + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts new file mode 100644 index 00000000000000..e6b4f67e6acfc2 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.test.ts @@ -0,0 +1,791 @@ +/* + * 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 { KibanaPrivileges, Role } from '../../../common/model'; +import { NO_PRIVILEGE_VALUE } from '../../views/management/edit_role/lib/constants'; +import { + buildRole, + defaultPrivilegeDefinition, + fullyRestrictedBasePrivileges, + fullyRestrictedFeaturePrivileges, + unrestrictedBasePrivileges, + unrestrictedFeaturePrivileges, +} from './__fixtures__'; +import { + AllowedPrivilege, + PRIVILEGE_SOURCE, + PrivilegeExplanation, +} from './kibana_privilege_calculator_types'; +import { KibanaPrivilegeCalculatorFactory } from './kibana_privileges_calculator_factory'; + +const buildEffectivePrivileges = ( + role: Role, + kibanaPrivileges: KibanaPrivileges = defaultPrivilegeDefinition +) => { + const factory = new KibanaPrivilegeCalculatorFactory(kibanaPrivileges); + return factory.getInstance(role); +}; + +const buildExpectedFeaturePrivileges = ( + expectedFeaturePrivileges: PrivilegeExplanation | { [featureId: string]: PrivilegeExplanation } +) => { + if (expectedFeaturePrivileges.hasOwnProperty('actualPrivilege')) { + return { + feature: { + feature1: expectedFeaturePrivileges, + feature2: expectedFeaturePrivileges, + feature3: expectedFeaturePrivileges, + }, + }; + } + + return { + feature: { + ...expectedFeaturePrivileges, + }, + }; +}; + +describe('calculateEffectivePrivileges', () => { + it(`returns an empty array for an empty role`, () => { + const role = buildRole(); + role.kibana = []; + + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + expect(calculatedPrivileges).toHaveLength(0); + }); + + it(`calculates "none" for all privileges when nothing is assigned`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo', 'bar'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }), + }, + ]); + }); + + describe(`with global base privilege of "all"`, () => { + it(`calculates global feature privileges === all`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }, + ]); + }); + + it(`calculates space base and feature privileges === all`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + const calculatedSpacePrivileges = calculatedPrivileges[1]; + + expect(calculatedSpacePrivileges).toEqual({ + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }); + }); + + describe(`and with feature privileges assigned`, () => { + it('returns the base privileges when they are more permissive', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['read'], + feature3: ['read'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['read'], + feature3: ['read'], + }, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + }), + }, + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + }), + }, + ]); + }); + }); + }); + + describe(`with global base privilege of "read"`, () => { + it(`it calculates space base and feature privileges when none are provided`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + ]); + }); + + describe('and with feature privileges assigned', () => { + it('returns the feature privileges when they are more permissive', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: { + feature1: ['all'], + feature2: ['all'], + feature3: ['all'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['all'], + feature2: ['all'], + feature3: ['all'], + }, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + feature2: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + feature3: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }), + }, + ]); + }); + }); + }); + + describe('with both global and space base privileges assigned', () => { + it(`does not override space base of "all" when global base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + isDirectlyAssigned: false, + }), + }, + ]); + }); + + it(`calcualtes "all" for space base and space features when superceded by global "all"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }, + { + base: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + }, + ...buildExpectedFeaturePrivileges({ + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }), + }, + ]); + }); + + it(`does not override feature privileges when they are more permissive`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: { + feature1: ['all'], + feature2: ['all'], + feature3: ['all'], + }, + }, + ], + }); + const effectivePrivileges = buildEffectivePrivileges(role); + const calculatedPrivileges = effectivePrivileges.calculateEffectivePrivileges(); + + expect(calculatedPrivileges).toEqual([ + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: true, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature2: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + }, + feature3: { + actualPrivilege: NO_PRIVILEGE_VALUE, + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + { + base: { + actualPrivilege: 'read', + actualPrivilegeSource: PRIVILEGE_SOURCE.GLOBAL_BASE, + isDirectlyAssigned: false, + supersededPrivilege: 'read', + supersededPrivilegeSource: PRIVILEGE_SOURCE.SPACE_BASE, + }, + ...buildExpectedFeaturePrivileges({ + feature1: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + feature2: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + feature3: { + actualPrivilege: 'all', + actualPrivilegeSource: PRIVILEGE_SOURCE.SPACE_FEATURE, + isDirectlyAssigned: true, + }, + }), + }, + ]); + }); + }); +}); + +describe('calculateAllowedPrivileges', () => { + it('allows all privileges when none are currently assigned', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + ]); + }); + + it('allows all global base privileges, but just "all" for everything else when global is set to "all"', () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + { + ...fullyRestrictedBasePrivileges, + ...fullyRestrictedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when global base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: ['read'], + feature: {}, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + const expectedFeaturePrivileges = { + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by global "all" + }, + }, + }; + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...expectedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: false, + }, + ...expectedFeaturePrivileges, + }, + ]); + }); + + it(`allows feature privileges to be set to "all" or "read" when space base is "read"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: {}, + }, + { + spaces: ['foo'], + base: ['read'], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); + + it(`allows space base privilege to be set to "all" or "read" when space base is already "all"`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: false, + }, + }, + }, + ]); + }); + + it(`restricts space feature privileges when global feature privileges are set`, () => { + const role = buildRole({ + spacesPrivileges: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['all'], + feature2: ['read'], + }, + }, + { + spaces: ['foo'], + base: [], + feature: {}, + }, + ], + }); + + const privilegeCalculator = buildEffectivePrivileges(role); + + const result: AllowedPrivilege[] = privilegeCalculator.calculateAllowedPrivileges(); + + expect(result).toEqual([ + { + ...unrestrictedBasePrivileges, + ...unrestrictedFeaturePrivileges, + }, + { + base: { + privileges: ['all', 'read'], + canUnassign: true, + }, + feature: { + feature1: { + privileges: ['all'], + canUnassign: false, + }, + feature2: { + privileges: ['all', 'read'], + canUnassign: false, + }, + feature3: { + privileges: ['all'], + canUnassign: true, // feature 3 has no "read" privilege governed by space "all" + }, + }, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts new file mode 100644 index 00000000000000..58c371e80290bc --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts @@ -0,0 +1,114 @@ +/* + * 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 { + FeaturesPrivileges, + KibanaPrivileges, + Role, + RoleKibanaPrivilege, +} from '../../../common/model'; +import { isGlobalPrivilegeDefinition } from '../privilege_utils'; +import { KibanaAllowedPrivilegesCalculator } from './kibana_allowed_privileges_calculator'; +import { KibanaBasePrivilegeCalculator } from './kibana_base_privilege_calculator'; +import { KibanaFeaturePrivilegeCalculator } from './kibana_feature_privilege_calculator'; +import { AllowedPrivilege, CalculatedPrivilege } from './kibana_privilege_calculator_types'; + +export class KibanaPrivilegeCalculator { + private allowedPrivilegesCalculator: KibanaAllowedPrivilegesCalculator; + + private effectiveBasePrivilegesCalculator: KibanaBasePrivilegeCalculator; + + private effectiveFeaturePrivilegesCalculator: KibanaFeaturePrivilegeCalculator; + + constructor( + private readonly kibanaPrivileges: KibanaPrivileges, + private readonly role: Role, + public readonly rankedFeaturePrivileges: FeaturesPrivileges + ) { + const globalPrivilege = this.locateGlobalPrivilege(role); + + const assignedGlobalBaseActions: string[] = globalPrivilege.base[0] + ? kibanaPrivileges.getGlobalPrivileges().getActions(globalPrivilege.base[0]) + : []; + + this.allowedPrivilegesCalculator = new KibanaAllowedPrivilegesCalculator( + kibanaPrivileges, + role + ); + + this.effectiveBasePrivilegesCalculator = new KibanaBasePrivilegeCalculator( + kibanaPrivileges, + globalPrivilege, + assignedGlobalBaseActions + ); + + this.effectiveFeaturePrivilegesCalculator = new KibanaFeaturePrivilegeCalculator( + kibanaPrivileges, + globalPrivilege, + assignedGlobalBaseActions, + rankedFeaturePrivileges + ); + } + + public calculateEffectivePrivileges(ignoreAssigned: boolean = false): CalculatedPrivilege[] { + const { kibana = [] } = this.role; + return kibana.map(privilegeSpec => + this.calculateEffectivePrivilege(privilegeSpec, ignoreAssigned) + ); + } + + public calculateAllowedPrivileges(): AllowedPrivilege[] { + const effectivePrivs = this.calculateEffectivePrivileges(true); + return this.allowedPrivilegesCalculator.calculateAllowedPrivileges(effectivePrivs); + } + + private calculateEffectivePrivilege( + privilegeSpec: RoleKibanaPrivilege, + ignoreAssigned: boolean + ): CalculatedPrivilege { + const result: CalculatedPrivilege = { + base: this.effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege( + privilegeSpec, + ignoreAssigned + ), + feature: {}, + reserved: privilegeSpec._reserved, + }; + + // If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is + // without ignoring assigned, in order to calculate the correct feature privileges. + const effectiveBase = ignoreAssigned + ? this.effectiveBasePrivilegesCalculator.getMostPermissiveBasePrivilege(privilegeSpec, false) + : result.base; + + const allFeaturePrivileges = this.kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + result.feature = Object.keys(allFeaturePrivileges).reduce((acc, featureId) => { + return { + ...acc, + [featureId]: this.effectiveFeaturePrivilegesCalculator.getMostPermissiveFeaturePrivilege( + privilegeSpec, + effectiveBase, + featureId, + ignoreAssigned + ), + }; + }, {}); + + return result; + } + + private locateGlobalPrivilege(role: Role) { + const spacePrivileges = role.kibana; + return ( + spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)) || { + spaces: [] as string[], + base: [] as string[], + feature: {}, + } + ); + } +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts new file mode 100644 index 00000000000000..41f6012737a920 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.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. + */ + +/** + * Describes the source of a privilege. + */ +export enum PRIVILEGE_SOURCE { + /** Privilege is assigned directly to the entity */ + SPACE_FEATURE = 10, + + /** Privilege is derived from space base privilege */ + SPACE_BASE = 20, + + /** Privilege is derived from global feature privilege */ + GLOBAL_FEATURE = 30, + + /** Privilege is derived from global base privilege */ + GLOBAL_BASE = 40, +} + +export interface PrivilegeExplanation { + actualPrivilege: string; + actualPrivilegeSource: PRIVILEGE_SOURCE; + isDirectlyAssigned: boolean; + supersededPrivilege?: string; + supersededPrivilegeSource?: PRIVILEGE_SOURCE; +} + +export interface CalculatedPrivilege { + base: PrivilegeExplanation; + feature: { + [featureId: string]: PrivilegeExplanation | undefined; + }; + reserved: undefined | string[]; +} + +export interface PrivilegeScenario { + actualPrivilegeSource: PRIVILEGE_SOURCE; + isDirectlyAssigned: boolean; + supersededPrivilege?: string; + supersededPrivilegeSource?: PRIVILEGE_SOURCE; + actions: string[]; +} + +export interface AllowedPrivilege { + base: { + privileges: string[]; + canUnassign: boolean; + }; + feature: { + [featureId: string]: + | { + privileges: string[]; + canUnassign: boolean; + } + | undefined; + }; +} diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts new file mode 100644 index 00000000000000..3d8a0698465ab2 --- /dev/null +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privileges_calculator_factory.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import _ from 'lodash'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../common/model'; +import { compareActions } from '../../../common/privilege_calculator_utils'; +import { copyRole } from '../../lib/role_utils'; +import { KibanaPrivilegeCalculator } from './kibana_privilege_calculator'; + +export class KibanaPrivilegeCalculatorFactory { + /** All feature privileges, sorted from most permissive => least permissive. */ + public readonly rankedFeaturePrivileges: FeaturesPrivileges; + + constructor(private readonly kibanaPrivileges: KibanaPrivileges) { + this.rankedFeaturePrivileges = {}; + const featurePrivilegeSet = kibanaPrivileges.getFeaturePrivileges().getAllPrivileges(); + + Object.entries(featurePrivilegeSet).forEach(([featureId, privileges]) => { + this.rankedFeaturePrivileges[featureId] = privileges.sort((privilege1, privilege2) => { + const privilege1Actions = kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege1); + const privilege2Actions = kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege2); + return compareActions(privilege1Actions, privilege2Actions); + }); + }); + } + + /** + * Creates an KibanaPrivilegeCalculator instance for the specified role. + * @param role + */ + public getInstance(role: Role) { + const roleCopy = copyRole(role); + + this.sortPrivileges(roleCopy); + return new KibanaPrivilegeCalculator( + this.kibanaPrivileges, + roleCopy, + this.rankedFeaturePrivileges + ); + } + + private sortPrivileges(role: Role) { + role.kibana.forEach(privilege => { + privilege.base.sort((privilege1, privilege2) => { + const privilege1Actions = this.kibanaPrivileges + .getSpacesPrivileges() + .getActions(privilege1); + + const privilege2Actions = this.kibanaPrivileges + .getSpacesPrivileges() + .getActions(privilege2); + + return compareActions(privilege1Actions, privilege2Actions); + }); + + Object.entries(privilege.feature).forEach(([featureId, featurePrivs]) => { + featurePrivs.sort((privilege1, privilege2) => { + const privilege1Actions = this.kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege1); + + const privilege2Actions = this.kibanaPrivileges + .getFeaturePrivileges() + .getActions(featureId, privilege2); + + return compareActions(privilege1Actions, privilege2Actions); + }); + }); + }); + } +} diff --git a/x-pack/plugins/security/public/lib/privilege_utils.test.ts b/x-pack/plugins/security/public/lib/privilege_utils.test.ts new file mode 100644 index 00000000000000..88ac71cbd76348 --- /dev/null +++ b/x-pack/plugins/security/public/lib/privilege_utils.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 { hasAssignedFeaturePrivileges, isGlobalPrivilegeDefinition } from './privilege_utils'; + +describe('isGlobalPrivilegeDefinition', () => { + it('returns true if no spaces are defined', () => { + expect( + // @ts-ignore + isGlobalPrivilegeDefinition({ + base: [], + feature: {}, + }) + ).toEqual(true); + }); + + it('returns true if spaces is an empty array', () => { + expect( + isGlobalPrivilegeDefinition({ + spaces: [], + base: [], + feature: {}, + }) + ).toEqual(true); + }); + + it('returns true if spaces contains "*"', () => { + expect( + isGlobalPrivilegeDefinition({ + spaces: ['*'], + base: [], + feature: {}, + }) + ).toEqual(true); + }); + + it('returns false if spaces does not contain "*"', () => { + expect( + isGlobalPrivilegeDefinition({ + spaces: ['foo', 'bar'], + base: [], + feature: {}, + }) + ).toEqual(false); + }); +}); + +describe('hasAssignedFeaturePrivileges', () => { + it('returns false if no feature privileges are defined', () => { + expect( + hasAssignedFeaturePrivileges({ + spaces: [], + base: [], + feature: {}, + }) + ).toEqual(false); + }); + + it('returns false if feature privileges are defined but not assigned', () => { + expect( + hasAssignedFeaturePrivileges({ + spaces: [], + base: [], + feature: { + foo: [], + }, + }) + ).toEqual(false); + }); + + it('returns true if feature privileges are defined and assigned', () => { + expect( + hasAssignedFeaturePrivileges({ + spaces: [], + base: [], + feature: { + foo: ['all'], + }, + }) + ).toEqual(true); + }); +}); diff --git a/x-pack/plugins/security/public/lib/privilege_utils.ts b/x-pack/plugins/security/public/lib/privilege_utils.ts new file mode 100644 index 00000000000000..74bde71dc421ab --- /dev/null +++ b/x-pack/plugins/security/public/lib/privilege_utils.ts @@ -0,0 +1,27 @@ +/* + * 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 { RoleKibanaPrivilege } from '../../common/model'; + +/** + * Determines if the passed privilege spec defines global privileges. + * @param privilegeSpec + */ +export function isGlobalPrivilegeDefinition(privilegeSpec: RoleKibanaPrivilege): boolean { + if (!privilegeSpec.spaces || privilegeSpec.spaces.length === 0) { + return true; + } + return privilegeSpec.spaces.includes('*'); +} + +/** + * Determines if the passed privilege spec defines feature privileges. + * @param privilegeSpec + */ +export function hasAssignedFeaturePrivileges(privilegeSpec: RoleKibanaPrivilege): boolean { + const featureKeys = Object.keys(privilegeSpec.feature); + return featureKeys.length > 0 && featureKeys.some(key => privilegeSpec.feature[key].length > 0); +} diff --git a/x-pack/plugins/security/public/lib/role.test.ts b/x-pack/plugins/security/public/lib/role_utils.test.ts similarity index 51% rename from x-pack/plugins/security/public/lib/role.test.ts rename to x-pack/plugins/security/public/lib/role_utils.test.ts index c86b250e034f6e..706d74b09c036c 100644 --- a/x-pack/plugins/security/public/lib/role.test.ts +++ b/x-pack/plugins/security/public/lib/role_utils.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isReservedRole, isRoleEnabled } from './role'; +import { Role } from '../../common/model'; +import { copyRole, isReadOnlyRole, isReservedRole, isRoleEnabled } from './role_utils'; describe('role', () => { describe('isRoleEnabled', () => { @@ -56,4 +57,64 @@ describe('role', () => { expect(isReservedRole(testRole)).toBe(false); }); }); + + describe('isReadOnlyRole', () => { + test('returns true for reserved roles', () => { + const testRole = { + metadata: { + _reserved: true, + }, + }; + expect(isReadOnlyRole(testRole)).toBe(true); + }); + + test('returns true for roles with transform errors', () => { + const testRole = { + _transform_error: ['kibana'], + }; + expect(isReadOnlyRole(testRole)).toBe(true); + }); + + test('returns false for all other roles', () => { + const testRole = {}; + expect(isReadOnlyRole(testRole)).toBe(false); + }); + }); + + describe('copyRole', () => { + it('should perform a deep copy', () => { + const role: Role = { + name: '', + elasticsearch: { + cluster: ['all'], + indices: [{ names: ['index*'], privileges: ['all'] }], + run_as: ['user'], + }, + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + { + spaces: ['default'], + base: ['foo'], + feature: {}, + }, + { + spaces: ['marketing'], + base: ['read'], + feature: {}, + }, + ], + }; + + const result = copyRole(role); + expect(result).toEqual(role); + + role.elasticsearch.indices[0].names = ['something else']; + + expect(result).not.toEqual(role); + }); + }); }); diff --git a/x-pack/plugins/security/public/lib/role.ts b/x-pack/plugins/security/public/lib/role_utils.ts similarity index 59% rename from x-pack/plugins/security/public/lib/role.ts rename to x-pack/plugins/security/public/lib/role_utils.ts index d6221f7aecb4c3..471a3b2ba112fc 100644 --- a/x-pack/plugins/security/public/lib/role.ts +++ b/x-pack/plugins/security/public/lib/role_utils.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get } from 'lodash'; -import { Role } from '../../common/model/role'; +import { cloneDeep, get } from 'lodash'; +import { Role } from '../../common/model'; /** * Returns whether given role is enabled or not @@ -25,3 +25,21 @@ export function isRoleEnabled(role: Partial) { export function isReservedRole(role: Partial) { return get(role, 'metadata._reserved', false); } + +/** + * Returns whether given role is editable through the UI or not. + * + * @param role the Role as returned by roles API + */ +export function isReadOnlyRole(role: Partial): boolean { + return isReservedRole(role) || !!(role._transform_error && role._transform_error.length > 0); +} + +/** + * Returns a deep copy of the role. + * + * @param role the Role to copy. + */ +export function copyRole(role: Role) { + return cloneDeep(role); +} diff --git a/x-pack/plugins/security/public/lib/transform_role_for_save.test.ts b/x-pack/plugins/security/public/lib/transform_role_for_save.test.ts new file mode 100644 index 00000000000000..1ea19f2637305e --- /dev/null +++ b/x-pack/plugins/security/public/lib/transform_role_for_save.test.ts @@ -0,0 +1,484 @@ +/* + * 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 { Role } from '../../common/model'; +import { transformRoleForSave } from './transform_role_for_save'; + +describe('transformRoleForSave', () => { + describe('spaces disabled', () => { + it('removes placeholder index privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: [], privileges: [] }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes placeholder query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'] }], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes transient fields not required for save', () => { + const role: Role = { + name: 'my role', + transient_metadata: { + foo: 'bar', + }, + _transform_error: ['kibana'], + metadata: { + someOtherMetadata: true, + }, + _unrecognized_applications: ['foo'], + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + metadata: { + someOtherMetadata: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('does not remove actual query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }); + }); + + it('should remove feature privileges if a corresponding base privilege is defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: ['all'], + feature: {}, + }, + ], + }); + }); + + it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + + it('should remove space privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, false); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + }); + + describe('spaces enabled', () => { + it('removes placeholder index privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: [], privileges: [] }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes placeholder query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: '' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'] }], + run_as: [], + }, + kibana: [], + }); + }); + + it('removes transient fields not required for save', () => { + const role: Role = { + name: 'my role', + transient_metadata: { + foo: 'bar', + }, + _transform_error: ['kibana'], + metadata: { + someOtherMetadata: true, + }, + _unrecognized_applications: ['foo'], + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + metadata: { + someOtherMetadata: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }); + }); + + it('does not remove actual query entries', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [{ names: ['.kibana*'], privileges: ['all'], query: 'something' }], + run_as: [], + }, + kibana: [], + }); + }); + + it('should remove feature privileges if a corresponding base privilege is defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: ['all'], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: ['all'], + feature: {}, + }, + ], + }); + }); + + it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['foo'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + + it('should not remove space privileges', () => { + const role: Role = { + name: 'my role', + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }; + + const result = transformRoleForSave(role, true); + + expect(result).toEqual({ + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + { + spaces: ['marketing'], + base: [], + feature: { + feature1: ['read'], + feature2: ['write'], + }, + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/security/public/lib/transform_role_for_save.ts b/x-pack/plugins/security/public/lib/transform_role_for_save.ts new file mode 100644 index 00000000000000..861ba530050a13 --- /dev/null +++ b/x-pack/plugins/security/public/lib/transform_role_for_save.ts @@ -0,0 +1,41 @@ +/* + * 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 { Role, RoleIndexPrivilege } from '../../common/model'; +import { isGlobalPrivilegeDefinition } from './privilege_utils'; + +export function transformRoleForSave(role: Role, spacesEnabled: boolean) { + // Remove any placeholder index privileges + role.elasticsearch.indices = role.elasticsearch.indices.filter( + indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) + ); + + // Remove any placeholder query entries + role.elasticsearch.indices.forEach(index => index.query || delete index.query); + + // If spaces are disabled, then do not persist any space privileges + if (!spacesEnabled) { + role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); + } + + role.kibana.forEach(kibanaPrivilege => { + // If a base privilege is defined, then do not persist feature privileges + if (kibanaPrivilege.base.length > 0) { + kibanaPrivilege.feature = {}; + } + }); + + delete role.name; + delete role.transient_metadata; + delete role._unrecognized_applications; + delete role._transform_error; + + return role; +} + +function isPlaceholderPrivilege(indexPrivilege: RoleIndexPrivilege) { + return indexPrivilege.names.length === 0; +} diff --git a/x-pack/plugins/security/public/objects/lib/roles.ts b/x-pack/plugins/security/public/objects/lib/roles.ts index 2551d7eabc4e71..e33cbe4c6c031c 100644 --- a/x-pack/plugins/security/public/objects/lib/roles.ts +++ b/x-pack/plugins/security/public/objects/lib/roles.ts @@ -3,14 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { omit } from 'lodash'; import chrome from 'ui/chrome'; -import { Role } from '../../../common/model/role'; +import { Role } from '../../../common/model'; +import { copyRole } from '../../lib/role_utils'; +import { transformRoleForSave } from '../../lib/transform_role_for_save'; const apiBase = chrome.addBasePath(`/api/security/role`); -export async function saveRole($http: any, role: Role) { - const data = omit(role, 'name', 'transient_metadata', '_unrecognized_applications'); +export async function saveRole($http: any, role: Role, spacesEnabled: boolean) { + const data = transformRoleForSave(copyRole(role), spacesEnabled); + return await $http.put(`${apiBase}/${role.name}`, data); } diff --git a/x-pack/plugins/security/public/views/_index.scss b/x-pack/plugins/security/public/views/_index.scss new file mode 100644 index 00000000000000..fa95d44f637e4d --- /dev/null +++ b/x-pack/plugins/security/public/views/_index.scss @@ -0,0 +1,8 @@ +// Public views +@import './logged_out/index'; + +// Login styles +@import './login/index'; + +// Management styles +@import './management/index'; diff --git a/x-pack/plugins/security/public/views/management/_index.scss b/x-pack/plugins/security/public/views/management/_index.scss new file mode 100644 index 00000000000000..d1064f58957397 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/_index.scss @@ -0,0 +1,2 @@ +@import './change_password_form/index'; +@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss b/x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss new file mode 100644 index 00000000000000..98331c2070a31e --- /dev/null +++ b/x-pack/plugins/security/public/views/management/change_password_form/_change_password_form.scss @@ -0,0 +1,17 @@ +.secChangePasswordForm__panel { + max-width: $secFormWidth; +} + +.secChangePasswordForm__subLabel { + margin-bottom: $euiSizeS; +} + +.secChangePasswordForm__footer { + display: flex; + justify-content: flex-start; + align-items: center; + + .kuiButton + .kuiButton { + margin-left: $euiSizeS; + } +} diff --git a/x-pack/plugins/security/public/views/management/change_password_form/_index.scss b/x-pack/plugins/security/public/views/management/change_password_form/_index.scss new file mode 100644 index 00000000000000..a6058b5ddebbf0 --- /dev/null +++ b/x-pack/plugins/security/public/views/management/change_password_form/_index.scss @@ -0,0 +1 @@ +@import './change_password_form'; diff --git a/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html b/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html index f5f717f27e9c54..92fb95861a6f8e 100644 --- a/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html +++ b/x-pack/plugins/security/public/views/management/change_password_form/change_password_form.html @@ -117,7 +117,7 @@
- diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx index d8aca1a9a4833f..4a2fd346658664 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx @@ -10,13 +10,6 @@ import { SpacesDescription } from './spaces_description'; describe('SpacesDescription', () => { it('renders without crashing', () => { - expect( - shallow( - true }} - onManageSpacesClick={jest.fn()} - /> - ) - ).toMatchSnapshot(); + expect(shallow()).toMatchSnapshot(); }); }); diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx index 9ac22c21c280a1..fe071dba254677 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx @@ -6,12 +6,10 @@ import { EuiContextMenuPanel, EuiText } from '@elastic/eui'; import React, { SFC } from 'react'; -import { UserProfile } from '../../../../../xpack_main/public/services/user_profile'; import { ManageSpacesButton } from '../../../components'; import { getSpacesFeatureDescription } from '../../../lib/constants'; interface Props { - userProfile: UserProfile; onManageSpacesClick: () => void; } @@ -30,7 +28,6 @@ export const SpacesDescription: SFC = (props: Props) => {
diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx index 825a14e7928bbc..76a47ca738627e 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx @@ -7,7 +7,6 @@ import { EuiContextMenuItem, EuiContextMenuPanel, EuiFieldSearch, EuiText } from '@elastic/eui'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { UserProfile } from '../../../../../xpack_main/public/services/user_profile'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants'; import { Space } from '../../../../common/model/space'; import { ManageSpacesButton, SpaceAvatar } from '../../../components'; @@ -16,7 +15,6 @@ interface Props { spaces: Space[]; onSelectSpace: (space: Space) => void; onManageSpacesClick: () => void; - userProfile: UserProfile; intl: InjectedIntl; } @@ -152,7 +150,6 @@ class SpacesMenuUI extends Component { key="manageSpacesButton" className="spcMenu__manageButton" size="s" - userProfile={this.props.userProfile} onClick={this.props.onManageSpacesClick} /> ); 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 6385e74280a12e..b07c30f8d7bd65 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 @@ -11,7 +11,6 @@ import template from 'plugins/spaces/views/nav_control/nav_control.html'; import { NavControlPopover } from 'plugins/spaces/views/nav_control/nav_control_popover'; // @ts-ignore import { PathProvider } from 'plugins/xpack_main/services/path'; -import { UserProfileProvider } from 'plugins/xpack_main/services/user_profile'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import ReactDOM from 'react-dom'; @@ -46,7 +45,6 @@ let spacesManager: SpacesManager; module.controller( 'spacesNavController', ($scope: any, $http: any, chrome: any, Private: any, activeSpace: any) => { - const userProfile = Private(UserProfileProvider); const pathProvider = Private(PathProvider); const domNode = document.getElementById(`spacesNavReactRoot`); @@ -63,7 +61,6 @@ module.controller( @@ -103,7 +100,6 @@ chromeHeaderNavControlsRegistry.register( order: 1000, side: NavControlSide.Left, render(el: HTMLElement) { - const userProfile = Private(UserProfileProvider); const pathProvider = Private(PathProvider); if (pathProvider.isUnauthenticated()) { @@ -119,7 +115,6 @@ chromeHeaderNavControlsRegistry.register( diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx index a1088ba91801a6..f67cc40762638b 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.tsx @@ -20,10 +20,12 @@ const createMockHttpAgent = (withSpaces = false) => { { id: '', name: 'space 1', + disabledFeatures: [], }, { id: '', name: 'space 2', + disabledFeatures: [], }, ]; @@ -42,7 +44,7 @@ const createMockHttpAgent = (withSpaces = false) => { describe('NavControlPopover', () => { it('renders without crashing', () => { const activeSpace = { - space: { id: '', name: 'foo' }, + space: { id: '', name: 'foo', disabledFeatures: [] }, valid: true, }; @@ -52,7 +54,6 @@ describe('NavControlPopover', () => { true }} anchorPosition={'downRight'} buttonClass={SpacesGlobalNavButton} /> @@ -62,7 +63,7 @@ describe('NavControlPopover', () => { it('renders a SpaceAvatar with the active space', async () => { const activeSpace = { - space: { id: '', name: 'foo' }, + space: { id: '', name: 'foo', disabledFeatures: [] }, valid: true, }; @@ -74,7 +75,6 @@ describe('NavControlPopover', () => { true }} anchorPosition={'rightCenter'} buttonClass={SpacesGlobalNavButton} /> diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx index 4ae7319b4d542d..4669bd3608e4f2 100644 --- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx +++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx @@ -6,7 +6,6 @@ import { EuiAvatar, EuiPopover, PopoverAnchorPosition } from '@elastic/eui'; import React, { Component, ComponentClass } from 'react'; -import { UserProfile } from '../../../../xpack_main/public/services/user_profile'; import { Space } from '../../../common/model/space'; import { SpaceAvatar } from '../../components'; import { SpacesManager } from '../../lib/spaces_manager'; @@ -21,7 +20,6 @@ interface Props { error?: string; space: Space; }; - userProfile: UserProfile; anchorPosition: PopoverAnchorPosition; buttonClass: ComponentClass; } @@ -62,18 +60,12 @@ export class NavControlPopover extends Component { let element: React.ReactNode; if (this.state.spaces.length < 2) { - element = ( - - ); + element = ; } else { element = ( ); diff --git a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx index f12b5b8ab8e1c7..5869ac9f7989ab 100644 --- a/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx +++ b/x-pack/plugins/spaces/public/views/space_selector/space_selector.test.tsx @@ -49,6 +49,7 @@ test('it uses the spaces on props, when provided', () => { id: 'space-1', name: 'Space 1', description: 'This is the first space', + disabledFeatures: [], }, ]; @@ -72,6 +73,7 @@ test('it queries for spaces when not provided on props', () => { id: 'space-1', name: 'Space 1', description: 'This is the first space', + disabledFeatures: [], }, ]; diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.test.ts b/x-pack/plugins/spaces/server/lib/create_default_space.test.ts index a71278a737188d..11ab5e71a0ea40 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.test.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.test.ts @@ -99,6 +99,7 @@ test(`it creates the default space when one does not exist`, async () => { { _reserved: true, description: 'This is your default space!', + disabledFeatures: [], name: 'Default', color: '#00bfb3', }, diff --git a/x-pack/plugins/spaces/server/lib/create_default_space.ts b/x-pack/plugins/spaces/server/lib/create_default_space.ts index f4ade584789244..fdcdbb1ef72b4e 100644 --- a/x-pack/plugins/spaces/server/lib/create_default_space.ts +++ b/x-pack/plugins/spaces/server/lib/create_default_space.ts @@ -40,6 +40,7 @@ export async function createDefaultSpace(server: any) { defaultMessage: 'This is your default space!', }), color: '#00bfb3', + disabledFeatures: [], _reserved: true, }, options diff --git a/x-pack/plugins/spaces/server/lib/migrations/index.ts b/x-pack/plugins/spaces/server/lib/migrations/index.ts new file mode 100644 index 00000000000000..b303a8489ffb09 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/migrations/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { migrateToKibana660 } from './migrate_6x'; diff --git a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts new file mode 100644 index 00000000000000..964eb8137685f6 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { migrateToKibana660 } from './migrate_6x'; + +describe('migrateTo660', () => { + it('adds a "disabledFeatures" attribute initialized as an empty array', () => { + expect( + migrateToKibana660({ + id: 'space:foo', + attributes: {}, + }) + ).toEqual({ + id: 'space:foo', + attributes: { + disabledFeatures: [], + }, + }); + }); + + it('does not initialize "disabledFeatures" if the property already exists', () => { + // This scenario shouldn't happen organically. Protecting against defects in the migration. + expect( + migrateToKibana660({ + id: 'space:foo', + attributes: { + disabledFeatures: ['foo', 'bar', 'baz'], + }, + }) + ).toEqual({ + id: 'space:foo', + attributes: { + disabledFeatures: ['foo', 'bar', 'baz'], + }, + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts new file mode 100644 index 00000000000000..0c080a8dabb0a7 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/migrations/migrate_6x.ts @@ -0,0 +1,12 @@ +/* + * 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 function migrateToKibana660(doc: Record) { + if (!doc.attributes.hasOwnProperty('disabledFeatures')) { + doc.attributes.disabledFeatures = []; + } + return doc; +} diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts new file mode 100644 index 00000000000000..2687a2c16295da --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; +import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; + +export function initSpacesRequestInterceptors(server: any) { + initSpacesOnRequestInterceptor(server); + initSpacesOnPostAuthRequestInterceptor(server); +} diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts new file mode 100644 index 00000000000000..0e6585ee489227 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.test.ts @@ -0,0 +1,404 @@ +/* + * 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 { Server } from 'hapi'; +import sinon from 'sinon'; + +import { SavedObject } from 'src/legacy/server/saved_objects'; +import { Feature } from '../../../../xpack_main/types'; +import { convertSavedObjectToSpace } from '../../routes/lib'; +import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; +import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; + +describe('onPostAuthRequestInterceptor', () => { + const sandbox = sinon.sandbox.create(); + const teardowns: Array<() => void> = []; + const headers = { + authorization: 'foo', + }; + let server: any; + let request: any; + + const serverBasePath = '/'; + const defaultRoute = '/app/custom-app'; + + beforeEach(() => { + teardowns.push(() => sandbox.restore()); + request = async ( + path: string, + spaces: SavedObject[], + setupFn: (server: Server) => null = () => null + ) => { + server = new Server(); + + interface Config { + [key: string]: any; + } + const config: Config = { + 'server.basePath': serverBasePath, + 'server.defaultRoute': defaultRoute, + }; + + server.decorate( + 'server', + 'config', + jest.fn(() => { + return { + get: jest.fn(key => { + return config[key]; + }), + }; + }) + ); + + server.savedObjects = { + SavedObjectsClient: { + errors: { + isNotFoundError: (e: Error) => e.message === 'space not found', + }, + }, + getSavedObjectsRepository: jest.fn().mockImplementation(() => { + return { + get: (type: string, id: string) => { + if (type === 'space') { + const space = spaces.find(s => s.id === id); + if (space) { + return space; + } + throw new Error('space not found'); + } + }, + create: () => null, + }; + }), + }; + + server.plugins = { + spaces: { + spacesClient: { + getScopedClient: jest.fn(), + }, + }, + xpack_main: { + getFeatures: () => + [ + { + id: 'feature-1', + name: 'feature 1', + app: ['app-1'], + }, + { + id: 'feature-2', + name: 'feature 2', + app: ['app-2'], + }, + { + id: 'feature-4', + name: 'feature 4', + app: ['app-1', 'app-4'], + }, + { + id: 'feature-5', + name: 'feature 4', + app: ['kibana'], + }, + ] as Feature[], + }, + }; + + let basePath: string | undefined; + server.decorate('request', 'getBasePath', () => basePath); + server.decorate('request', 'setBasePath', (newPath: string) => { + basePath = newPath; + }); + + // The onRequest interceptor is also included here because the onPostAuth interceptor requires the onRequest + // interceptor to parse out the space id and rewrite the request's URL. Rather than duplicating that logic, + // we are including the already tested interceptor here in the test chain. + initSpacesOnRequestInterceptor(server); + initSpacesOnPostAuthRequestInterceptor(server); + + server.route([ + { + method: 'GET', + path: '/', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/app/{appId}', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/api/foo', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + ]); + + teardowns.push(() => server.stop()); + + server.plugins.spaces.spacesClient.getScopedClient.mockReturnValue({ + getAll() { + return spaces.map(convertSavedObjectToSpace); + }, + get(spaceId: string) { + const space = spaces.find(s => s.id === spaceId); + if (!space) { + throw new Error('space not found'); + } + return convertSavedObjectToSpace(space); + }, + }); + + await setupFn(server); + + return await server.inject({ + method: 'GET', + url: path, + headers, + }); + }; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + describe('when accessing an app within a non-existent space', () => { + it('redirects to the space selector screen', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + ]; + + const response = await request('/s/not-found/app/kibana', spaces); + + expect(response.statusCode).toEqual(302); + expect(response.headers.location).toEqual(serverBasePath); + }); + }); + + it('when accessing the kibana app it always allows the request to continue', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: ['feature-1', 'feature-2', 'feature-4', 'feature-5'], + }, + }, + ]; + + const response = await request('/s/a-space/app/kibana', spaces); + + expect(response.statusCode).toEqual(200); + }); + + describe('when accessing an API endpoint within a non-existent space', () => { + it('allows the request to continue', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + ]; + + const response = await request('/s/not-found/api/foo', spaces); + + expect(response.statusCode).toEqual(200); + }); + }); + + describe('with a single available space', () => { + test('it redirects to the defaultRoute within the context of the single Space when navigating to Kibana root', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + ]; + + const response = await request('/', spaces); + + expect(response.statusCode).toEqual(302); + expect(response.headers.location).toEqual(`${serverBasePath}/s/a-space${defaultRoute}`); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { + // This is very similar to the test above, but this handles the condition where the only available space is the Default Space, + // which does not have a URL Context. In this scenario, the end result is the same as the other test, but the final URL the user + // is redirected to does not contain a space identifier (e.g., /s/foo) + + const spaces = [ + { + id: 'default', + type: 'space', + attributes: { + name: 'Default Space', + }, + }, + ]; + + const response = await request('/', spaces); + + expect(response.statusCode).toEqual(302); + expect(response.headers.location).toEqual(`${serverBasePath}${defaultRoute}`); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('it allows navigation to apps when none are disabled', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: [] as any, + }, + }, + ]; + + const response = await request('/s/a-space/app/kibana', spaces); + + expect(response.statusCode).toEqual(200); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('allows navigation to app that is granted by multiple features, when only one of those features is disabled', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: ['feature-1'] as any, + }, + }, + ]; + + const response = await request('/s/a-space/app/app-1', spaces); + + expect(response.statusCode).toEqual(200); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + + test('does not allow navigation to apps that are only provided by a disabled feature', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + disabledFeatures: ['feature-2'] as any, + }, + }, + ]; + + const response = await request('/s/a-space/app/app-2', spaces); + + expect(response.statusCode).toEqual(404); + + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + }); + + describe('with multiple available spaces', () => { + test('it redirects to the Space Selector App when navigating to Kibana root', async () => { + const spaces = [ + { + id: 'a-space', + type: 'space', + attributes: { + name: 'a space', + }, + }, + { + id: 'b-space', + type: 'space', + attributes: { + name: 'b space', + }, + }, + ]; + + const getHiddenUiAppHandler = jest.fn(() => '
space selector
'); + + const response = await request('/', spaces, function setupFn() { + server.decorate('server', 'getHiddenUiAppById', getHiddenUiAppHandler); + server.decorate('toolkit', 'renderApp', function renderAppHandler(app: any) { + // @ts-ignore + return this.response(app); + }); + }); + + expect(response.statusCode).toEqual(200); + expect(response.payload).toEqual('
space selector
'); + + expect(getHiddenUiAppHandler).toHaveBeenCalledTimes(1); + expect(getHiddenUiAppHandler).toHaveBeenCalledWith('space_selector'); + expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( + expect.objectContaining({ + headers: expect.objectContaining({ + authorization: headers.authorization, + }), + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.ts similarity index 52% rename from x-pack/plugins/spaces/server/lib/space_request_interceptors.ts rename to x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.ts index baf1bdc061c164..c5aaded074c34b 100644 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.ts +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_post_auth_interceptor.ts @@ -3,42 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import Boom from 'boom'; +import { Server } from 'hapi'; +import { Space } from '../../../common/model/space'; +import { wrapError } from '../errors'; +import { getSpaceSelectorUrl } from '../get_space_selector_url'; +import { SpacesClient } from '../spaces_client'; +import { addSpaceIdToPath, getSpaceIdFromPath } from '../spaces_url_parser'; + +interface KbnServer extends Server { + getHiddenUiAppById: (appId: string) => any; +} -import { DEFAULT_SPACE_ID } from '../../common/constants'; -import { wrapError } from './errors'; -import { getSpaceSelectorUrl } from './get_space_selector_url'; -import { addSpaceIdToPath, getSpaceIdFromPath } from './spaces_url_parser'; - -export function initSpacesRequestInterceptors(server: any) { - const serverBasePath = server.config().get('server.basePath'); - - server.ext('onRequest', async function spacesOnRequestHandler(request: any, h: any) { - const path = request.path; - - // If navigating within the context of a space, then we store the Space's URL Context on the request, - // and rewrite the request to not include the space identifier in the URL. - const spaceId = getSpaceIdFromPath(path, serverBasePath); - - if (spaceId !== DEFAULT_SPACE_ID) { - const reqBasePath = `/s/${spaceId}`; - request.setBasePath(reqBasePath); - - const newLocation = path.substr(reqBasePath.length) || '/'; - - const newUrl = { - ...request.url, - path: newLocation, - pathname: newLocation, - href: newLocation, - }; - - request.setUrl(newUrl); - } - - return h.continue; - }); +export function initSpacesOnPostAuthRequestInterceptor(server: KbnServer) { + const serverBasePath: string = server.config().get('server.basePath'); + const xpackMainPlugin = server.plugins.xpack_main; - server.ext('onPostAuth', async function spacesOnRequestHandler(request: any, h: any) { + server.ext('onPostAuth', async function spacesOnPostAuthHandler(request: any, h: any) { const path = request.path; const isRequestingKibanaRoot = path === '/'; @@ -53,8 +34,8 @@ export function initSpacesRequestInterceptors(server: any) { const spaces = await spacesClient.getAll(); const config = server.config(); - const basePath = config.get('server.basePath'); - const defaultRoute = config.get('server.defaultRoute'); + const basePath: string = config.get('server.basePath'); + const defaultRoute: string = config.get('server.defaultRoute'); if (spaces.length === 1) { // If only one space is available, then send user there directly. @@ -78,14 +59,17 @@ export function initSpacesRequestInterceptors(server: any) { // This condition should only happen after selecting a space, or when transitioning from one application to another // e.g.: Navigating from Dashboard to Timelion if (isRequestingApplication) { - let spaceId; + let spaceId: string = ''; + let space: Space; try { - const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request); + const spacesClient: SpacesClient = server.plugins.spaces.spacesClient.getScopedClient( + request + ); spaceId = getSpaceIdFromPath(request.getBasePath(), serverBasePath); server.log(['spaces', 'debug'], `Verifying access to space "${spaceId}"`); - await spacesClient.get(spaceId); + space = await spacesClient.get(spaceId); } catch (error) { server.log( ['spaces', 'error'], @@ -94,6 +78,31 @@ export function initSpacesRequestInterceptors(server: any) { // Space doesn't exist, or user not authorized for space, or some other issue retrieving the active space. return h.redirect(getSpaceSelectorUrl(server.config())).takeover(); } + + // Verify application is available in this space + // The management page is always visible, so we shouldn't be restricting access to the kibana application in any situation. + const appId = path.split('/', 3)[2]; + if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) { + server.log(['spaces', 'debug'], `Verifying application is available: "${appId}"`); + + const allFeatures = xpackMainPlugin.getFeatures(); + + const isRegisteredApp = allFeatures.some(feature => feature.app.includes(appId)); + if (isRegisteredApp) { + const enabledFeatures = allFeatures.filter( + feature => !space.disabledFeatures.includes(feature.id) + ); + + const isAvailableInSpace = enabledFeatures.some(feature => feature.app.includes(appId)); + if (!isAvailableInSpace) { + server.log( + ['spaces', 'error'], + `App ${appId} is not enabled within space "${spaceId}".` + ); + return Boom.notFound(); + } + } + } } return h.continue; }); diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts new file mode 100644 index 00000000000000..a8868f805049f3 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.test.ts @@ -0,0 +1,194 @@ +/* + * 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 { Server } from 'hapi'; +import sinon from 'sinon'; + +import { initSpacesOnRequestInterceptor } from './on_request_interceptor'; + +describe('onRequestInterceptor', () => { + const sandbox = sinon.sandbox.create(); + const teardowns: Array<() => void> = []; + const headers = { + authorization: 'foo', + }; + let server: any; + let request: any; + + beforeEach(() => { + teardowns.push(() => sandbox.restore()); + request = async (path: string) => { + server = new Server(); + + interface Config { + [key: string]: any; + } + const config: Config = { + 'server.basePath': '/foo', + }; + + server.decorate( + 'server', + 'config', + jest.fn(() => { + return { + get: jest.fn(key => { + return config[key]; + }), + }; + }) + ); + + server.savedObjects = { + SavedObjectsClient: { + errors: { + isNotFoundError: (e: Error) => e.message === 'space not found', + }, + }, + getSavedObjectsRepository: jest.fn().mockImplementation(() => { + return { + get: (type: string, id: string) => { + if (type === 'space') { + if (id === 'not-found') { + throw new Error('space not found'); + } + return { + id, + name: 'test space', + }; + } + }, + create: () => null, + }; + }), + }; + + server.plugins = { + spaces: { + spacesClient: { + getScopedClient: jest.fn(), + }, + }, + }; + + initSpacesOnRequestInterceptor(server); + + server.route([ + { + method: 'GET', + path: '/', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/app/kibana', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/some/path/s/foo/bar', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/i/love/spaces', + handler: (req: any) => { + return { path: req.path, query: req.query, url: req.url, basePath: req.getBasePath() }; + }, + }, + { + method: 'GET', + path: '/api/foo', + handler: (req: any) => { + return { path: req.path, url: req.url, basePath: req.getBasePath() }; + }, + }, + ]); + + let basePath: string | undefined; + server.decorate('request', 'getBasePath', () => basePath); + server.decorate('request', 'setBasePath', (newPath: string) => { + basePath = newPath; + }); + + teardowns.push(() => server.stop()); + + return await server.inject({ + method: 'GET', + url: path, + headers, + }); + }; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + describe('onRequest', () => { + test('handles paths without a space identifier', async () => { + const response = await request('/'); + + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/', + url: { + path: '/', + pathname: '/', + href: '/', + }, + }); + }); + + test('strips the Space URL Context from the request', async () => { + const response = await request('/s/foo'); + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/', + url: { + path: '/', + pathname: '/', + href: '/', + }, + }); + }); + + test('ignores space identifiers in the middle of the path', async () => { + const response = await request('/some/path/s/foo/bar'); + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/some/path/s/foo/bar', + url: { + path: '/some/path/s/foo/bar', + pathname: '/some/path/s/foo/bar', + href: '/some/path/s/foo/bar', + }, + }); + }); + + test('strips the Space URL Context from the request, maintaining the rest of the path', async () => { + const response = await request('/s/foo/i/love/spaces?queryParam=queryValue'); + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchObject({ + path: '/i/love/spaces', + query: { + queryParam: 'queryValue', + }, + url: { + path: '/i/love/spaces', + pathname: '/i/love/spaces', + href: '/i/love/spaces', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.ts new file mode 100644 index 00000000000000..9a5161eeab5f61 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/request_inteceptors/on_request_interceptor.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 { Server } from 'hapi'; +import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { getSpaceIdFromPath } from '../spaces_url_parser'; + +export function initSpacesOnRequestInterceptor(server: Server) { + const serverBasePath: string = server.config().get('server.basePath'); + + server.ext('onRequest', async function spacesOnRequestHandler(request: any, h: any) { + const path = request.path; + + // If navigating within the context of a space, then we store the Space's URL Context on the request, + // and rewrite the request to not include the space identifier in the URL. + const spaceId = getSpaceIdFromPath(path, serverBasePath); + + if (spaceId !== DEFAULT_SPACE_ID) { + const reqBasePath = `/s/${spaceId}`; + request.setBasePath(reqBasePath); + + const newLocation = path.substr(reqBasePath.length) || '/'; + + const newUrl = { + ...request.url, + path: newLocation, + pathname: newLocation, + href: newLocation, + }; + + request.setUrl(newUrl); + } + + return h.continue; + }); +} diff --git a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts b/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts deleted file mode 100644 index cef8fa78831915..00000000000000 --- a/x-pack/plugins/spaces/server/lib/space_request_interceptors.test.ts +++ /dev/null @@ -1,440 +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 { Server } from 'hapi'; -import sinon from 'sinon'; - -import { SavedObject } from 'src/legacy/server/saved_objects'; -import { initSpacesRequestInterceptors } from './space_request_interceptors'; - -describe('interceptors', () => { - const sandbox = sinon.sandbox.create(); - const teardowns: Array<() => void> = []; - const headers = { - authorization: 'foo', - }; - let server: any; - let request: any; - - beforeEach(() => { - teardowns.push(() => sandbox.restore()); - request = async ( - path: string, - setupFn: (ser: any) => void = () => { - return; - }, - testConfig = {} - ) => { - server = new Server(); - - interface Config { - [key: string]: any; - } - const config: Config = { - 'server.basePath': '/foo', - ...testConfig, - }; - - server.decorate( - 'server', - 'config', - jest.fn(() => { - return { - get: jest.fn(key => { - return config[key]; - }), - }; - }) - ); - - server.savedObjects = { - SavedObjectsClient: { - errors: { - isNotFoundError: (e: Error) => e.message === 'space not found', - }, - }, - getSavedObjectsRepository: jest.fn().mockImplementation(() => { - return { - get: (type: string, id: string) => { - if (type === 'space') { - if (id === 'not-found') { - throw new Error('space not found'); - } - return { - id, - name: 'test space', - }; - } - }, - create: () => null, - }; - }), - }; - - server.plugins = { - spaces: { - spacesClient: { - getScopedClient: jest.fn(), - }, - }, - }; - - initSpacesRequestInterceptors(server); - - server.route([ - { - method: 'GET', - path: '/', - handler: (req: any) => { - return { path: req.path, url: req.url, basePath: req.getBasePath() }; - }, - }, - { - method: 'GET', - path: '/app/kibana', - handler: (req: any) => { - return { path: req.path, url: req.url, basePath: req.getBasePath() }; - }, - }, - { - method: 'GET', - path: '/api/foo', - handler: (req: any) => { - return { path: req.path, url: req.url, basePath: req.getBasePath() }; - }, - }, - ]); - - await setupFn(server); - - let basePath: string | undefined; - server.decorate('request', 'getBasePath', () => basePath); - server.decorate('request', 'setBasePath', (newPath: string) => { - basePath = newPath; - }); - - teardowns.push(() => server.stop()); - - return await server.inject({ - method: 'GET', - url: path, - headers, - }); - }; - }); - - afterEach(async () => { - await Promise.all(teardowns.splice(0).map(fn => fn())); - }); - - describe('onRequest', () => { - test('handles paths without a space identifier', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/'); - return h.continue; - }); - - await request('/', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - - test('strips the Space URL Context from the request', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/'); - return h.continue; - }); - - await request('/s/foo', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - - test('ignores space identifiers in the middle of the path', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/some/path/s/foo/bar'); - return h.continue; - }); - - await request('/some/path/s/foo/bar', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - - test('strips the Space URL Context from the request, maintaining the rest of the path', async () => { - const testHandler = jest.fn((req, h) => { - expect(req.path).toBe('/i/love/spaces.html'); - expect(req.query).toEqual({ - queryParam: 'queryValue', - }); - return h.continue; - }); - - await request('/s/foo/i/love/spaces.html?queryParam=queryValue', (hapiServer: any) => { - hapiServer.ext('onRequest', testHandler); - }); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - }); - - describe('onPostAuth', () => { - const serverBasePath = '/my/base/path'; - const defaultRoute = '/app/custom-app'; - - const config = { - 'server.basePath': serverBasePath, - 'server.defaultRoute': defaultRoute, - }; - - const setupTest = (hapiServer: any, spaces: SavedObject[], testHandler: any) => { - hapiServer.plugins.spaces.spacesClient.getScopedClient.mockReturnValue({ - getAll() { - return spaces; - }, - }); - - // Register test inspector - hapiServer.ext('onPreResponse', testHandler); - }; - - describe('when accessing an app within a non-existent space', () => { - it('redirects to the space selector screen', async () => { - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(302); - expect(response.headers.location).toEqual(serverBasePath); - - return h.continue; - }); - - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - ]; - - await request( - '/s/not-found/app/kibana', - (hapiServer: any) => { - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - }); - - describe('when accessing an API endpoint within a non-existent space', () => { - it('allows the request to continue', async () => { - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(200); - - return h.continue; - }); - - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - ]; - - await request( - '/s/not-found/api/foo', - (hapiServer: any) => { - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - }); - }); - - describe('with a single available space', () => { - test('it redirects to the defaultRoute within the context of the single Space when navigating to Kibana root', async () => { - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(302); - expect(response.headers.location).toEqual(`${serverBasePath}/s/a-space${defaultRoute}`); - - return h.continue; - }); - - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - ]; - - await request( - '/', - (hapiServer: any) => { - setupTest(server, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }) - ); - }); - - test('it redirects to the defaultRoute within the context of the Default Space when navigating to Kibana root', async () => { - // This is very similar to the test above, but this handles the condition where the only available space is the Default Space, - // which does not have a URL Context. In this scenario, the end result is the same as the other test, but the final URL the user - // is redirected to does not contain a space identifier (e.g., /s/foo) - - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(302); - expect(response.headers.location).toEqual(`${serverBasePath}${defaultRoute}`); - - return h.continue; - }); - - const spaces = [ - { - id: 'default', - type: 'space', - attributes: { - name: 'Default Space', - }, - references: [], - }, - ]; - - await request( - '/', - (hapiServer: any) => { - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(testHandler).toHaveBeenCalledTimes(1); - expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }) - ); - }); - }); - - describe('with multiple available spaces', () => { - test('it redirects to the Space Selector App when navigating to Kibana root', async () => { - const spaces = [ - { - id: 'a-space', - type: 'space', - attributes: { - name: 'a space', - }, - references: [], - }, - { - id: 'b-space', - type: 'space', - attributes: { - name: 'b space', - }, - references: [], - }, - ]; - - const getHiddenUiAppHandler = jest.fn(() => '
space selector
'); - - const testHandler = jest.fn((req, h) => { - const { response } = req; - - if (response && response.isBoom) { - throw response; - } - - expect(response.statusCode).toEqual(200); - expect(response.source).toEqual({ app: '
space selector
', renderApp: true }); - - return h.continue; - }); - - await request( - '/', - (hapiServer: any) => { - server.decorate('server', 'getHiddenUiAppById', getHiddenUiAppHandler); - server.decorate('toolkit', 'renderApp', function renderAppHandler(app: any) { - // @ts-ignore - this({ renderApp: true, app }); - }); - - setupTest(hapiServer, spaces, testHandler); - }, - config - ); - - expect(getHiddenUiAppHandler).toHaveBeenCalledTimes(1); - expect(getHiddenUiAppHandler).toHaveBeenCalledWith('space_selector'); - expect(testHandler).toHaveBeenCalledTimes(1); - expect(server.plugins.spaces.spacesClient.getScopedClient).toHaveBeenCalledWith( - expect.objectContaining({ - headers: expect.objectContaining({ - authorization: headers.authorization, - }), - }) - ); - }); - }); - }); -}); diff --git a/x-pack/plugins/spaces/server/lib/space_schema.ts b/x-pack/plugins/spaces/server/lib/space_schema.ts index d27a855e0f9170..0ffaf9ce51cf85 100644 --- a/x-pack/plugins/spaces/server/lib/space_schema.ts +++ b/x-pack/plugins/spaces/server/lib/space_schema.ts @@ -13,5 +13,6 @@ export const spaceSchema = Joi.object({ description: Joi.string().allow(''), initials: Joi.string().max(MAX_SPACE_INITIALS), color: Joi.string().regex(/^#[a-z0-9]{6}$/, `6 digit hex color, starting with a #`), + disabledFeatures: Joi.array().items(Joi.string()), _reserved: Joi.boolean(), }).default(); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts index 360a51ecdd7984..92213bd5dce160 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts @@ -25,7 +25,9 @@ const createMockAuthorization = () => { const mockAuthorization = { actions: { login: 'action:login', - manageSpaces: 'action:manageSpaces', + space: { + manage: 'space:manage', + }, }, checkPrivilegesWithRequest: jest.fn(() => ({ atSpaces: mockCheckPrivilegesAtSpaces, @@ -367,7 +369,7 @@ describe('#canEnumerateSpaces', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -401,7 +403,7 @@ describe('#canEnumerateSpaces', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -570,12 +572,14 @@ describe('#create', () => { description: 'foo-description', bar: 'foo-bar', _reserved: true, + disabledFeatures: [], }; const attributes = { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }; const savedObject = { @@ -584,6 +588,7 @@ describe('#create', () => { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }, }; @@ -592,6 +597,7 @@ describe('#create', () => { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }; describe(`authorization is null`, () => { @@ -788,7 +794,7 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'create'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -840,7 +846,7 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); @@ -889,7 +895,7 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); @@ -904,12 +910,14 @@ describe('#update', () => { description: 'foo-description', bar: 'foo-bar', _reserved: false, + disabledFeatures: [], }; const attributes = { name: 'foo-name', description: 'foo-description', bar: 'foo-bar', + disabledFeatures: [], }; const savedObject = { @@ -919,6 +927,7 @@ describe('#update', () => { description: 'foo-description', bar: 'foo-bar', _reserved: true, + disabledFeatures: [], }, }; @@ -928,6 +937,7 @@ describe('#update', () => { description: 'foo-description', bar: 'foo-bar', _reserved: true, + disabledFeatures: [], }; describe(`authorization is null`, () => { @@ -1021,7 +1031,7 @@ describe('#update', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'update'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -1059,7 +1069,7 @@ describe('#update', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockInternalRepository.update).toHaveBeenCalledWith('space', id, attributes); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); @@ -1240,7 +1250,7 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'delete'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -1275,7 +1285,7 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -1314,7 +1324,7 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.manageSpaces + mockAuthorization.actions.space.manage ); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockInternalRepository.delete).toHaveBeenCalledWith('space', id); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client.ts index 52085cf6b2e5a7..5d14cb99447c11 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client.ts @@ -24,7 +24,7 @@ export class SpacesClient { if (this.useRbac()) { const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request); const { hasAllRequested } = await checkPrivileges.globally( - this.authorization.actions.manageSpaces + this.authorization.actions.space.manage ); this.debugLogger(`SpacesClient.canEnumerateSpaces, using RBAC. Result: ${hasAllRequested}`); return hasAllRequested; @@ -121,7 +121,7 @@ export class SpacesClient { this.debugLogger(`SpacesClient.create(), using RBAC. Checking if authorized globally`); await this.ensureAuthorizedGlobally( - this.authorization.actions.manageSpaces, + this.authorization.actions.space.manage, 'create', 'Unauthorized to create spaces' ); @@ -157,7 +157,7 @@ export class SpacesClient { public async update(id: string, space: Space) { if (this.useRbac()) { await this.ensureAuthorizedGlobally( - this.authorization.actions.manageSpaces, + this.authorization.actions.space.manage, 'update', 'Unauthorized to update spaces' ); @@ -175,7 +175,7 @@ export class SpacesClient { public async delete(id: string) { if (this.useRbac()) { await this.ensureAuthorizedGlobally( - this.authorization.actions.manageSpaces, + this.authorization.actions.space.manage, 'delete', 'Unauthorized to delete spaces' ); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts new file mode 100644 index 00000000000000..fb9c8a85a4e2e0 --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.test.ts @@ -0,0 +1,169 @@ +/* + * 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 { UICapabilities } from 'ui/capabilities'; +import { Feature } from '../../../xpack_main/types'; +import { Space } from '../../common/model/space'; +import { toggleUICapabilities } from './toggle_ui_capabilities'; + +const features: Feature[] = [ + { + id: 'feature_1', + name: 'Feature 1', + app: [], + privileges: {}, + }, + { + id: 'feature_2', + name: 'Feature 2', + navLinkId: 'feature2', + app: [], + catalogue: ['feature2Entry'], + management: { + kibana: ['somethingElse'], + }, + privileges: { + all: { + app: [], + ui: [], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, + { + id: 'feature_3', + name: 'Feature 3', + navLinkId: 'feature3', + app: [], + catalogue: ['feature3Entry'], + management: { + kibana: ['indices'], + }, + privileges: { + all: { + app: [], + ui: [], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, +]; + +const buildUiCapabilities = () => + Object.freeze({ + navLinks: { + feature1: true, + feature2: true, + feature3: true, + unknownFeature: true, + }, + catalogue: { + discover: true, + visualize: false, + }, + management: { + kibana: { + settings: false, + indices: true, + somethingElse: true, + }, + }, + feature_1: { + foo: true, + bar: true, + }, + feature_2: { + foo: true, + bar: true, + }, + feature_3: { + foo: true, + bar: true, + }, + }) as UICapabilities; + +describe('toggleUiCapabilities', () => { + it('does not toggle capabilities when the space has no disabled features', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: [], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + expect(result).toEqual(buildUiCapabilities()); + }); + + it('ignores unknown disabledFeatures', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['i-do-not-exist'], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + expect(result).toEqual(buildUiCapabilities()); + }); + + it('disables the corresponding navLink, catalogue, management sections, and all capability flags for disabled features', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_2'], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + + const expectedCapabilities = buildUiCapabilities(); + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + + expect(result).toEqual(expectedCapabilities); + }); + + it('can disable everything', () => { + const space: Space = { + id: 'space', + name: '', + disabledFeatures: ['feature_1', 'feature_2', 'feature_3'], + }; + + const uiCapabilities: UICapabilities = buildUiCapabilities(); + const result = toggleUICapabilities(features, uiCapabilities, space); + + const expectedCapabilities = buildUiCapabilities(); + + expectedCapabilities.feature_1.bar = false; + expectedCapabilities.feature_1.foo = false; + + expectedCapabilities.navLinks.feature2 = false; + expectedCapabilities.catalogue.feature2Entry = false; + expectedCapabilities.management.kibana.somethingElse = false; + expectedCapabilities.feature_2.bar = false; + expectedCapabilities.feature_2.foo = false; + + expectedCapabilities.navLinks.feature3 = false; + expectedCapabilities.catalogue.feature3Entry = false; + expectedCapabilities.management.kibana.indices = false; + expectedCapabilities.feature_3.bar = false; + expectedCapabilities.feature_3.foo = false; + + expect(result).toEqual(expectedCapabilities); + }); +}); diff --git a/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts new file mode 100644 index 00000000000000..4cec018959ab3e --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/toggle_ui_capabilities.ts @@ -0,0 +1,71 @@ +/* + * 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'; +import { Feature } from '../../../xpack_main/types'; +import { Space } from '../../common/model/space'; + +export function toggleUICapabilities( + features: Feature[], + uiCapabilities: UICapabilities, + activeSpace: Space +) { + const clonedCapabilities = _.cloneDeep(uiCapabilities); + + toggleDisabledFeatures(features, clonedCapabilities, activeSpace); + + return clonedCapabilities; +} + +function toggleDisabledFeatures( + features: Feature[], + uiCapabilities: UICapabilities, + activeSpace: Space +) { + const disabledFeatureKeys: string[] = activeSpace.disabledFeatures; + + const disabledFeatures: Feature[] = disabledFeatureKeys + .map(key => features.find(feature => feature.id === key)) + .filter(feature => typeof feature !== 'undefined') as Feature[]; + + const navLinks: Record = uiCapabilities.navLinks; + const catalogueEntries: Record = uiCapabilities.catalogue; + const managementItems: Record> = uiCapabilities.management; + + for (const feature of disabledFeatures) { + // Disable associated navLink, if one exists + if (feature.navLinkId && navLinks.hasOwnProperty(feature.navLinkId)) { + navLinks[feature.navLinkId] = false; + } + + // Disable associated catalogue entries + const privilegeCatalogueEntries: string[] = feature.catalogue || []; + privilegeCatalogueEntries.forEach(catalogueEntryId => { + catalogueEntries[catalogueEntryId] = false; + }); + + // Disable associated management items + const privilegeManagementSections: Record = feature.management || {}; + Object.entries(privilegeManagementSections).forEach(([sectionId, sectionItems]) => { + sectionItems.forEach(item => { + if ( + managementItems.hasOwnProperty(sectionId) && + managementItems[sectionId].hasOwnProperty(item) + ) { + managementItems[sectionId][item] = false; + } + }); + }); + + // Disable "sub features" that match the disabled feature + if (uiCapabilities.hasOwnProperty(feature.id)) { + const capability = uiCapabilities[feature.id]; + Object.keys(capability).forEach(featureKey => { + capability[featureKey] = false; + }); + } + } +} diff --git a/x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.ts b/x-pack/plugins/spaces/types.d.ts similarity index 60% rename from x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.ts rename to x-pack/plugins/spaces/types.d.ts index 395f14756c5470..98a20203c13c12 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/lib/copy_role.ts +++ b/x-pack/plugins/spaces/types.d.ts @@ -3,10 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { Request } from 'hapi'; -import { cloneDeep } from 'lodash'; -import { Role } from '../../../../../common/model/role'; - -export function copyRole(role: Role) { - return cloneDeep(role); +export interface SpacesPlugin { + getSpaceId(request: Record): string; } diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e125b6263fe863..82d0c1a1decf9d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7457,75 +7457,41 @@ "xpack.security.management.editRole.setPrivilegesToKibanaSpacesDescription": "设置 Elasticsearch 数据的权限并控制对 Kibana 空间的访问权限。", "xpack.security.management.editRole.updateRoleText": "更新角色", "xpack.security.management.editRole.viewingRoleTitle": "正在查看角色", - "xpack.security.management.editRoles.deleteRoleButton.cancelButtonLabel": "不,不删除", - "xpack.security.management.editRoles.deleteRoleButton.confirmButtonLabel": "是的,删除角色", - "xpack.security.management.editRoles.deleteRoleButton.deleteRoleButtonLabel": "删除角色", - "xpack.security.management.editRoles.deleteRoleButton.deleteRoleTitle": "删除角色", - "xpack.security.management.editRoles.deleteRoleButton.deletingRoleConfirmationText": "是否确定要删除此角色?", - "xpack.security.management.editRoles.deleteRoleButton.deletingRoleWarningText": "此操作无法撤消!", - "xpack.security.management.editRoles.elasticSearchPrivileges.addIndexPrivilegesButtonLabel": "添加索引权限", - "xpack.security.management.editRoles.elasticSearchPrivileges.addUserTitle": "添加用户……", - "xpack.security.management.editRoles.elasticSearchPrivileges.clusterPrivilegesTitle": "集群权限", - "xpack.security.management.editRoles.elasticSearchPrivileges.controlAccessToClusterDataDescription": "控制对集群中数据的访问权限。", - "xpack.security.management.editRoles.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription": "允许代表其他用户提交请求。", - "xpack.security.management.editRoles.elasticSearchPrivileges.indexPrivilegesTitle": "索引权限", - "xpack.security.management.editRoles.elasticSearchPrivileges.learnMoreLinkText": "了解详情", - "xpack.security.management.editRoles.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。", - "xpack.security.management.editRoles.elasticSearchPrivileges.runAsPrivilegesTitle": "以权限角色运行", - "xpack.security.management.editRoles.impactedSpacesFlyout.allLabel": "全部", - "xpack.security.management.editRoles.impactedSpacesFlyout.noneLabel": "无", - "xpack.security.management.editRoles.impactedSpacesFlyout.readLabel": "读取", - "xpack.security.management.editRoles.impactedSpacesFlyout.spacePrivilegesSummaryTitle": "工作区权限摘要", - "xpack.security.management.editRoles.impactedSpacesFlyout.viewSpacesPrivilegesSummaryLinkText": "查看工作区权限摘要", - "xpack.security.management.editRoles.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限", - "xpack.security.management.editRoles.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "授权的文档查询", - "xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowHelpText": "如果未授权任何字段,则分配到此角色的用户将无法查看此索引的任何数据。", - "xpack.security.management.editRoles.indexPrivilegeForm.grantedFieldsFormRowLabel": "授权字段(可选)", - "xpack.security.management.editRoles.indexPrivilegeForm.grantReadPrivilegesLabel": "向特定文档授予读取权限", - "xpack.security.management.editRoles.indexPrivilegeForm.indicesFormRowLabel": "索引", - "xpack.security.management.editRoles.indexPrivilegeForm.privilegesFormRowLabel": "权限", - "xpack.security.management.editRoles.privilegeCalloutWarning.allText": "全部", - "xpack.security.management.editRoles.privilegeCalloutWarning.alwaysGrantReadAccessToAllSpacesTitle": "此角色始终授予对所有空间的读取权限。要定制各个空间的权限,必须创建一个新角色。", - "xpack.security.management.editRoles.privilegeCalloutWarning.howToCustomizePrivilegesDescription": "此角色始终授予对所有空间的完全访问权限。要定制各个空间的权限,必须创建一个新角色。", - "xpack.security.management.editRoles.privilegeCalloutWarning.howToCustomizePrivilegesForIndividualSpacesDescription": "将最低权限设置为 {allText} 可以授予对所有空间的完全访问权限。要定制各个空间的权限,最低权限必须为 {readText} 或 {noneText}。", - "xpack.security.management.editRoles.privilegeCalloutWarning.minimalPossiblePrivilageTitle": "可能的最低权限是 {readText}。", - "xpack.security.management.editRoles.privilegeCalloutWarning.minimumPrivilegeTitle": "最低权限太高,无法定制各个空间", - "xpack.security.management.editRoles.privilegeCalloutWarning.neverGrantReadAccessToAllSpacesTitle": "此角色永远不会授予对 Kibana 内任何空间的访问权限。要定制各个空间的权限,必须创建一个新角色。", - "xpack.security.management.editRoles.privilegeCalloutWarning.noneText": "无", - "xpack.security.management.editRoles.privilegeCalloutWarning.notPossibleToCustomizeReservedRoleSpacePrivilegesTitle": "无法定制保留角色的空间权限", - "xpack.security.management.editRoles.privilegeCalloutWarning.readText": "读取", - "xpack.security.management.editRoles.privilegeSpaceForm.deleteSpacePrivilegeAriaLabel": "删除空间权限", - "xpack.security.management.editRoles.privilegeSpaceForm.privilegeFormRowLabel": "权限", - "xpack.security.management.editRoles.privilegeSpaceForm.spacesFormRowLabel": "工作区", - "xpack.security.management.editRoles.privilegeSpaceTable.actionsName": "操作", - "xpack.security.management.editRoles.privilegeSpaceTable.deletedSpaceDescription": "{value}(已删除)", - "xpack.security.management.editRoles.privilegeSpaceTable.filterPlaceholder": "筛选", - "xpack.security.management.editRoles.privilegeSpaceTable.privilegeName": "权限", - "xpack.security.management.editRoles.privilegeSpaceTable.spaceName": "工作区", - "xpack.security.management.editRoles.reversedRoleBadget.reversedRolesCanNotBeRemovedTooltip": "保留的角色是内置的,无法删除或修改。", - "xpack.security.management.editRoles.simplePrivilegeForm.kibanaPrivilegesTitle": "Kibana 权限", - "xpack.security.management.editRoles.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "为此角色指定 Kibana 权限。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.addSpacePrivilegeTitle": "添加工作区权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.allText": "全部", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.grantMorePrivilegesTitle": "基于每个工作区授予更多权限。例如,如果所有工作区的权限均为 {read},则可以将单个工作区的权限设置为 {all}。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.higherPrivilegesForIndividualSpacesTitle": "针对各个工作区的更高权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您没有权限查看所有可用工作区。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.minimumActionsUserCanPerformInYourSpacesDescription": "指定用户可在您的工作区中执行的最少操作。", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.minPrivilegesForAllSpacesTitle": "针对所有工作区的最低权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.noAccessToSpacesHelpText": "无工作区访问权限", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.readText": "读取", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.viewEditShareAppsWithinAllSpacesHelpText": "在所有工作区内查看、编辑和共享对象及应用", - "xpack.security.management.editRoles.spaceAwarePrivilegeForm.viewObjectsAndAppsWithinAllSpacesHelpText": "在所有工作区内查看对象和应用", - "xpack.security.management.editRoles.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为一个数组", - "xpack.security.management.editRoles.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", - "xpack.security.management.editRoles.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", - "xpack.security.management.editRoles.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", - "xpack.security.management.editRoles.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", - "xpack.security.management.editRoles.validateRole.privilegeRequiredWarningMessage": "“权限”必填", - "xpack.security.management.editRoles.validateRole.provideRoleNameWarningMessage": "请提供一个角色名称", + "xpack.security.management.editRole.deleteRoleButton.cancelButtonLabel": "不,不删除", + "xpack.security.management.editRole.deleteRoleButton.confirmButtonLabel": "是的,删除角色", + "xpack.security.management.editRole.deleteRoleButton.deleteRoleButtonLabel": "删除角色", + "xpack.security.management.editRole.deleteRoleButton.deleteRoleTitle": "删除角色", + "xpack.security.management.editRole.deleteRoleButton.deletingRoleConfirmationText": "是否确定要删除此角色?", + "xpack.security.management.editRole.deleteRoleButton.deletingRoleWarningText": "此操作无法撤消!", + "xpack.security.management.editRole.elasticSearchPrivileges.addIndexPrivilegesButtonLabel": "添加索引权限", + "xpack.security.management.editRole.elasticSearchPrivileges.addUserTitle": "添加用户……", + "xpack.security.management.editRole.elasticSearchPrivileges.clusterPrivilegesTitle": "集群权限", + "xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription": "控制对集群中数据的访问权限。", + "xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription": "允许代表其他用户提交请求。", + "xpack.security.management.editRole.elasticSearchPrivileges.indexPrivilegesTitle": "索引权限", + "xpack.security.management.editRole.elasticSearchPrivileges.learnMoreLinkText": "了解详情", + "xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription": "管理此角色可以对您的集群执行的操作。", + "xpack.security.management.editRole.elasticSearchPrivileges.runAsPrivilegesTitle": "以权限角色运行", + "xpack.security.management.editRole.indexPrivilegeForm.deleteSpacePrivilegeAriaLabel": "删除索引权限", + "xpack.security.management.editRole.indexPrivilegeForm.grantedDocumentsQueryFormRowLabel": "授权的文档查询", + "xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowHelpText": "如果未授权任何字段,则分配到此角色的用户将无法查看此索引的任何数据。", + "xpack.security.management.editRole.indexPrivilegeForm.grantedFieldsFormRowLabel": "授权字段(可选)", + "xpack.security.management.editRole.indexPrivilegeForm.grantReadPrivilegesLabel": "向特定文档授予读取权限", + "xpack.security.management.editRole.indexPrivilegeForm.indicesFormRowLabel": "索引", + "xpack.security.management.editRole.indexPrivilegeForm.privilegesFormRowLabel": "权限", + "xpack.security.management.editRole.simplePrivilegeForm.kibanaPrivilegesTitle": "Kibana 权限", + "xpack.security.management.editRole.simplePrivilegeForm.specifyPrivilegeForRoleDescription": "为此角色指定 Kibana 权限。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.ensureAccountHasAllPrivilegesGrantedDescription": "请确保您的帐户具有 {kibanaUser} 角色授予的所有权限,然后重试。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.howToViewAllAvailableSpacesDescription": "您没有权限查看所有可用工作区。", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.insufficientPrivilegesDescription": "权限不足", + "xpack.security.management.editRole.spaceAwarePrivilegeForm.kibanaUserTitle": "kibana_user", + "xpack.security.management.editRole.validateRole.indicesTypeErrorMessage": "{elasticIndices} 应为一个数组", + "xpack.security.management.editRole.validateRole.nameAllowedCharactersWarningMessage": "名称必须以字母或下划线开头,且只能包含字母、下划线和数字。", + "xpack.security.management.editRole.validateRole.nameLengthWarningMessage": "名称不能超过 1024 个字符", + "xpack.security.management.editRole.validateRole.onePrivilegeRequiredWarningMessage": "至少需要一个权限", + "xpack.security.management.editRole.validateRole.oneSpaceRequiredWarningMessage": "至少需要一个工作区", + "xpack.security.management.editRole.validateRole.privilegeRequiredWarningMessage": "“权限”必填", + "xpack.security.management.editRole.validateRole.provideRoleNameWarningMessage": "请提供一个角色名称", "xpack.security.management.passwordForm.confirmPasswordLabel": "确认密码", "xpack.security.management.passwordForm.passwordDontMatchDescription": "密码不匹配", "xpack.security.management.passwordForm.passwordLabel": "密码", @@ -7624,33 +7590,20 @@ "xpack.spaces.management.confirmDeleteModal.redirectAfterDeletingCurrentSpaceWarningMessage": "您即将删除当前空间 {name}。如果继续,系统会将您重定向到选择其他空间的位置。", "xpack.spaces.management.confirmDeleteModal.spaceNamesDoNoMatchErrorMessage": "空间名称不匹配。", "xpack.spaces.management.customizeSpaceAvatar.colorFormRowLabel": "颜色", - "xpack.spaces.management.customizeSpaceAvatar.customizeLinkText": "定制", "xpack.spaces.management.customizeSpaceAvatar.initialItemsFormRowLabel": "名字缩写(最多两个字符)", "xpack.spaces.management.deleteSpacesButton.deleteSpaceAriaLabel": "删除此空间", "xpack.spaces.management.deleteSpacesButton.deleteSpaceButtonLabel": "删除空间", "xpack.spaces.management.deleteSpacesButton.deleteSpaceErrorTitle": "删除空间时出错:{errorMessage}", "xpack.spaces.management.deleteSpacesButton.spaceSuccessfullyDeletedNotificationMessage": "已删除 {spaceName} 空间。", - "xpack.spaces.management.editSpace.manageSpacePage.optionalDescriptionFormRowLabel": "描述(可选)", - "xpack.spaces.management.manageSpacePage.avatarLabel": "头像", "xpack.spaces.management.manageSpacePage.awesomeSpacePlaceholder": "超卓的空间", - "xpack.spaces.management.manageSpacePage.cancelButtonLabel": "取消", - "xpack.spaces.management.manageSpacePage.createSpaceButtonLabel": "创建空间", "xpack.spaces.management.manageSpacePage.createSpaceTitle": "创建一个空间", - "xpack.spaces.management.manageSpacePage.editSpaceTitle": "编辑空间", "xpack.spaces.management.manageSpacePage.errorLoadingSpaceTitle": "加载空间时出错:{message}", "xpack.spaces.management.manageSpacePage.errorSavingSpaceTitle": "保存空间时出错:{message}", - "xpack.spaces.management.manageSpacePage.hereMagicHappensPlaceholder": "实现您想要的功能。", - "xpack.spaces.management.manageSpacePage.loadingTitle": "正在加载……", "xpack.spaces.management.manageSpacePage.nameFormRowLabel": "名称", "xpack.spaces.management.manageSpacePage.spaceSuccessfullySavedNotificationMessage": "空间 “{name}” 已保存。", - "xpack.spaces.management.manageSpacePage.updateSpaceButtonLabel": "更新空间", "xpack.spaces.management.reversedSpaceBadge.reversedSpacesCanBePartiallyModifiedTooltip": "保留的空间是内置的,只能进行部分修改。", "xpack.spaces.management.secureSpaceMessage.howToAssignRoleToSpaceDescription": "想要为空间分配角色?请转到“管理”并选择 {rolesLink}。", "xpack.spaces.management.secureSpaceMessage.rolesLinkText": "角色", - "xpack.spaces.management.spaceIdentifier.editSpaceLinkText": "[编辑]", - "xpack.spaces.management.spaceIdentifier.engineeringText": "工程", - "xpack.spaces.management.spaceIdentifier.kibanaURLForEngineeringIdentifierDescription": "如果标识符为 {engineeringIdentifier},则 Kibana URL 为{nextLine} {engineeringKibanaUrl}。", - "xpack.spaces.management.spaceIdentifier.stopEditingSpaceNameLinkText": "[停止编辑]", "xpack.spaces.management.spaceIdentifier.urlIdentifierGeneratedFromSpaceNameTooltip": "URL 标识符基于空间名称生成。", "xpack.spaces.management.spaceIdentifier.urlIdentifierLabel": "URL 标识符", "xpack.spaces.management.spaceIdentifier.urlIdentifierTitle": "URL 标识符", diff --git a/x-pack/plugins/uptime/index.ts b/x-pack/plugins/uptime/index.ts index 2a88e61acf1c0e..959de2ede3d9e3 100644 --- a/x-pack/plugins/uptime/index.ts +++ b/x-pack/plugins/uptime/index.ts @@ -5,10 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { Server } from 'hapi'; import { resolve } from 'path'; import { PLUGIN } from './common/constants'; -import { initServerWithKibana } from './server'; +import { initServerWithKibana, KibanaServer } from './server'; export const uptime = (kibana: any) => new kibana.Plugin({ @@ -33,7 +32,7 @@ export const uptime = (kibana: any) => }, home: ['plugins/uptime/register_feature'], }, - init(server: Server) { + init(server: KibanaServer) { initServerWithKibana(server); }, }); diff --git a/x-pack/plugins/uptime/public/uptime_app.tsx b/x-pack/plugins/uptime/public/uptime_app.tsx index 1927530ceb38f4..f4d4acd4305641 100644 --- a/x-pack/plugins/uptime/public/uptime_app.tsx +++ b/x-pack/plugins/uptime/public/uptime_app.tsx @@ -140,7 +140,7 @@ const Application = (props: UptimeAppProps) => { }} > - +
diff --git a/x-pack/plugins/uptime/server/index.ts b/x-pack/plugins/uptime/server/index.ts index 1e3c022c31d50f..ca7ca9a10031f7 100644 --- a/x-pack/plugins/uptime/server/index.ts +++ b/x-pack/plugins/uptime/server/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { initServerWithKibana } from './kibana.index'; +export { initServerWithKibana, KibanaServer } from './kibana.index'; diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index b6d9f256951dcd..b0225383f43ce3 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -4,7 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import { Request, Server } from 'hapi'; +import { PLUGIN } from '../common/constants'; import { compose } from './lib/compose/kibana'; import { initUptimeServer } from './uptime_server'; @@ -16,11 +18,41 @@ export interface KibanaRouteOptions { options: any; } -export interface KibanaServer { +export interface KibanaServer extends Server { route: (options: KibanaRouteOptions) => void; } -export const initServerWithKibana = (server: Server) => { +export const initServerWithKibana = (server: KibanaServer) => { const libs = compose(server); initUptimeServer(libs); + + const xpackMainPlugin = server.plugins.xpack_main; + xpackMainPlugin.registerFeature({ + id: PLUGIN.ID, + name: i18n.translate('xpack.uptime.featureRegistry.uptimeFeatureName', { + defaultMessage: 'Uptime', + }), + navLinkId: PLUGIN.ID, + icon: 'uptimeApp', + app: ['uptime', 'kibana'], + catalogue: ['uptime'], + privileges: { + all: { + api: ['uptime'], + savedObject: { + all: [], + read: ['config'], + }, + ui: [], + }, + read: { + api: ['uptime'], + savedObject: { + all: [], + read: ['config'], + }, + ui: [], + }, + }, + }); }; diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts index c0ca2fa08d81bf..ffd5b5e970b7bf 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/kibana_framework_adapter.ts @@ -37,6 +37,9 @@ export class UMKibanaBackendFrameworkAdapter implements UMBackendFrameworkAdapte schema, }), path: routePath, + route: { + tags: ['access:uptime'], + }, }, plugin: uptimeGraphQLHapiPlugin, }); diff --git a/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts b/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts index 35661fe817845a..15024f7c5f4978 100644 --- a/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts +++ b/x-pack/plugins/uptime/server/rest_api/auth/is_valid.ts @@ -10,4 +10,7 @@ export const createIsValidRoute = (libs: UMServerLibs) => ({ method: 'GET', path: '/api/uptime/is_valid', handler: async (request: any): Promise => await libs.auth.requestIsValid(request), + options: { + tags: ['access:uptime'], + }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts index d0dcc835d58013..70f9a5c0611912 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_all.ts @@ -22,6 +22,7 @@ export const createGetAllRoute = (libs: UMServerLibs) => ({ status: Joi.string(), }), }, + tags: ['access:uptime'], }, handler: async (request: any): Promise => { const { size, sort, dateRangeStart, dateRangeEnd, monitorId, status } = request.query; diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js index 4ba75d950806db..9ef88097e8e0da 100644 --- a/x-pack/plugins/xpack_main/index.js +++ b/x-pack/plugins/xpack_main/index.js @@ -17,6 +17,7 @@ import { getLocalizationUsageCollector } from './server/lib/get_localization_usa import { xpackInfoRoute, telemetryRoute, + featuresRoute, settingsRoute, } from './server/routes/api/v1'; import { @@ -27,6 +28,7 @@ import mappings from './mappings.json'; import { i18n } from '@kbn/i18n'; export { callClusterFactory } from './server/lib/call_cluster_factory'; +import { registerOssFeatures } from './server/lib/register_oss_features'; /** * Determine if Telemetry is enabled. @@ -96,7 +98,6 @@ export const xpackMain = (kibana) => { telemetryOptedIn: null, activeSpace: null, spacesEnabled: config.get('xpack.spaces.enabled'), - userProfile: {}, }; }, hacks: [ @@ -121,11 +122,13 @@ export const xpackMain = (kibana) => { mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); setupXPackMain(server); + registerOssFeatures(server.plugins.xpack_main.registerFeature); // register routes xpackInfoRoute(server); telemetryRoute(server); settingsRoute(server, this.kbnServer); + featuresRoute(server); server.usage.collectorSet.register(getLocalizationUsageCollector(server)); } }); diff --git a/x-pack/plugins/xpack_main/public/services/user_profile.test.ts b/x-pack/plugins/xpack_main/public/services/user_profile.test.ts deleted file mode 100644 index 45507ab6042844..00000000000000 --- a/x-pack/plugins/xpack_main/public/services/user_profile.test.ts +++ /dev/null @@ -1,42 +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 { UserProfileProvider } from './user_profile'; - -describe('UserProfile', () => { - it('should return true when the specified capability is enabled', () => { - const capabilities = { - test1: true, - test2: false, - }; - - const userProfile = UserProfileProvider(capabilities); - - expect(userProfile.hasCapability('test1')).toEqual(true); - }); - - it('should return false when the specified capability is disabled', () => { - const capabilities = { - test1: true, - test2: false, - }; - - const userProfile = UserProfileProvider(capabilities); - - expect(userProfile.hasCapability('test2')).toEqual(false); - }); - - it('should return the default value when the specified capability is not defined', () => { - const capabilities = { - test1: true, - test2: false, - }; - - const userProfile = UserProfileProvider(capabilities); - - expect(userProfile.hasCapability('test3')).toEqual(true); - expect(userProfile.hasCapability('test3', false)).toEqual(false); - }); -}); diff --git a/x-pack/plugins/xpack_main/public/services/user_profile.ts b/x-pack/plugins/xpack_main/public/services/user_profile.ts deleted file mode 100644 index 09b257aa80e3fe..00000000000000 --- a/x-pack/plugins/xpack_main/public/services/user_profile.ts +++ /dev/null @@ -1,31 +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. - */ - -interface Capabilities { - [capability: string]: boolean; -} - -export interface UserProfile { - hasCapability: (capability: string) => boolean; -} - -export function UserProfileProvider(userProfile: Capabilities) { - class UserProfileClass implements UserProfile { - private capabilities: Capabilities; - - constructor(profileData: Capabilities = {}) { - this.capabilities = { - ...profileData, - }; - } - - public hasCapability(capability: string, defaultValue: boolean = true): boolean { - return capability in this.capabilities ? this.capabilities[capability] : defaultValue; - } - } - - return new UserProfileClass(userProfile); -} diff --git a/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js index 26bce06adaabac..263e03946cb176 100644 --- a/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js +++ b/x-pack/plugins/xpack_main/server/lib/__tests__/replace_injected_vars.js @@ -47,7 +47,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); sinon.assert.calledOnce(server.plugins.security.isAuthenticated); @@ -67,7 +72,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -84,7 +94,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -101,7 +116,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -118,7 +138,12 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); @@ -135,32 +160,53 @@ describe('replaceInjectedVars uiExport', () => { xpackInitialInfo: { b: 1 }, - userProfile: {}, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, }); }); - it('sends the originalInjectedVars if not authenticated', async () => { + it('sends the originalInjectedVars augmented with UI Capabilities if not authenticated', async () => { const originalInjectedVars = { a: 1 }; const request = buildRequest(); const server = mockServer(); server.plugins.security.isAuthenticated.returns(false); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.be(originalInjectedVars); + expect(newVars).to.eql({ + ...originalInjectedVars, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, + }); }); - it('sends the originalInjectedVars if xpack info is unavailable', async () => { + it('sends the originalInjectedVars augmented with UI Capabilities if xpack info is unavailable', async () => { const originalInjectedVars = { a: 1 }; const request = buildRequest(); const server = mockServer(); server.plugins.xpack_main.info.isAvailable.returns(false); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.be(originalInjectedVars); + expect(newVars).to.eql({ + ...originalInjectedVars, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, + }); }); it('sends the originalInjectedVars (with xpackInitialInfo = undefined) if security is disabled, xpack info is unavailable', async () => { - const originalInjectedVars = { a: 1 }; + const originalInjectedVars = { a: 1, uiCapabilities: { navLinks: { foo: true }, bar: { baz: true }, catalogue: { cfoo: true } } }; const request = buildRequest(); const server = mockServer(); delete server.plugins.security; @@ -171,18 +217,35 @@ describe('replaceInjectedVars uiExport', () => { a: 1, telemetryOptedIn: null, xpackInitialInfo: undefined, - userProfile: {}, + uiCapabilities: { + navLinks: { foo: true }, + bar: { baz: true }, + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: { + cfoo: true, + } + }, }); }); - it('sends the originalInjectedVars if the license check result is not available', async () => { + it('sends the originalInjectedVars augmented with UI Capabilities if the license check result is not available', async () => { const originalInjectedVars = { a: 1 }; const request = buildRequest(); const server = mockServer(); server.plugins.xpack_main.info.feature().getLicenseCheckResults.returns(undefined); const newVars = await replaceInjectedVars(originalInjectedVars, request, server); - expect(newVars).to.be(originalInjectedVars); + expect(newVars).to.eql({ + ...originalInjectedVars, + uiCapabilities: { + mockFeature: { + mockFeatureCapability: true, + }, + catalogue: {} + }, + }); }); }); @@ -196,6 +259,20 @@ function mockServer() { isAuthenticated: sinon.stub().returns(true) }, xpack_main: { + getFeatures: () => [{ + id: 'mockFeature', + name: 'Mock Feature', + privileges: { + all: { + app: [], + savedObject: { + all: [], + read: [], + }, + ui: ['mockFeatureCapability'] + } + } + }], info: { isAvailable: sinon.stub().returns(true), feature: () => ({ diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap new file mode 100644 index 00000000000000..1271f757d066e2 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/__snapshots__/feature_registry.test.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FeatureRegistry prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; + +exports[`FeatureRegistry prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`; + +exports[`FeatureRegistry prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; + +exports[`FeatureRegistry prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts new file mode 100644 index 00000000000000..cfe6719489b9ec --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts @@ -0,0 +1,338 @@ +/* + * 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 { Feature, FeatureRegistry } from './feature_registry'; + +describe('FeatureRegistry', () => { + it('allows a minimal feature to be registered', () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: {}, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + const result = featureRegistry.getAll(); + expect(result).toHaveLength(1); + + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0]).not.toBe(feature); + expect(result[0]).toEqual(feature); + }); + + it('allows a complex feature to be registered', () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + description: 'this is a rather boring feature description !@#$%^&*()_+-=\\[]{}|;\':"/.,<>?', + icon: 'addDataApp', + navLinkId: 'someNavLink', + app: ['app1', 'app2'], + validLicenses: ['standard', 'basic', 'gold', 'platinum'], + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + privileges: { + all: { + grantWithBaseRead: true, + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + }, + privilegesTooltip: 'some fancy tooltip', + reserved: { + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + description: 'some completely adequate description', + }, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + const result = featureRegistry.getAll(); + expect(result).toHaveLength(1); + + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0]).not.toBe(feature); + expect(result[0]).toEqual(feature); + }); + + it(`does not allow duplicate features to be registered`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: {}, + }; + + const duplicateFeature: Feature = { + id: 'test-feature', + name: 'Duplicate Test Feature', + app: [], + privileges: {}, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature); + + expect(() => featureRegistry.register(duplicateFeature)).toThrowErrorMatchingInlineSnapshot( + `"Feature with id test-feature is already registered."` + ); + }); + + ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach(prohibitedId => { + it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.register({ + id: prohibitedId, + name: 'some feature', + app: [], + privileges: {}, + }) + ).toThrowErrorMatchingSnapshot(); + }); + }); + + it('prevents features from being registered with invalid privilege names', () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['app1', 'app2'], + privileges: { + foo: { + app: ['app1', 'app2'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` + ); + }); + + it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown app entries: foo, baz"` + ); + }); + + it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: {}, + reserved: { + description: 'something', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` + ); + }); + + it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: { + all: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown catalogue entries: foo, baz"` + ); + }); + + it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: {}, + reserved: { + description: 'something', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` + ); + }); + + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + }, + privileges: { + all: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown management section: elasticsearch"` + ); + }); + + it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + }, + privileges: {}, + reserved: { + description: 'something', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` + ); + }); + + it('cannot register feature after getAll has been called', () => { + const feature1: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: {}, + }; + const feature2: Feature = { + id: 'test-feature-2', + name: 'Test Feature 2', + app: [], + privileges: {}, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.register(feature1); + featureRegistry.getAll(); + expect(() => { + featureRegistry.register(feature2); + }).toThrowErrorMatchingInlineSnapshot(`"Features are locked, can't register new features"`); + }); +}); 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 new file mode 100644 index 00000000000000..e536529f7d6d24 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts @@ -0,0 +1,196 @@ +/* + * 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 Joi from 'joi'; +import { cloneDeep, difference } from 'lodash'; +import { UICapabilities } from 'ui/capabilities'; + +export interface FeatureKibanaPrivileges { + grantWithBaseRead?: boolean; + management?: { + [sectionId: string]: string[]; + }; + catalogue?: string[]; + api?: string[]; + app?: string[]; + savedObject: { + all: string[]; + read: string[]; + }; + ui: string[]; +} + +type PrivilegesSet = Record; + +export type FeatureWithAllOrReadPrivileges = Feature<{ + all?: FeatureKibanaPrivileges; + read?: FeatureKibanaPrivileges; +}>; + +export interface Feature = PrivilegesSet> { + id: string; + name: string; + validLicenses?: Array<'basic' | 'standard' | 'gold' | 'platinum'>; + icon?: string; + description?: string; + navLinkId?: string; + app: string[]; + management?: { + [sectionId: string]: string[]; + }; + catalogue?: string[]; + privileges: TPrivileges; + privilegesTooltip?: string; + reserved?: { + privilege: FeatureKibanaPrivileges; + description: string; + }; +} + +// Each feature gets its own property on the UICapabilities object, +// but that object has a few built-in properties which should not be overwritten. +const prohibitedFeatureIds: Array = ['catalogue', 'management', 'navLinks']; + +const featurePrivilegePartRegex = /^[a-zA-Z0-9_-]+$/; +const managementSectionIdRegex = /^[a-zA-Z0-9_-]+$/; +export const uiCapabilitiesRegex = /^[a-zA-Z0-9:_-]+$/; + +const managementSchema = Joi.object().pattern( + managementSectionIdRegex, + Joi.array().items(Joi.string()) +); +const catalogueSchema = Joi.array().items(Joi.string()); + +const privilegeSchema = Joi.object({ + grantWithBaseRead: Joi.bool(), + management: managementSchema, + catalogue: catalogueSchema, + api: Joi.array().items(Joi.string()), + app: Joi.array().items(Joi.string()), + savedObject: Joi.object({ + all: Joi.array() + .items(Joi.string()) + .required(), + read: Joi.array() + .items(Joi.string()) + .required(), + }).required(), + ui: Joi.array() + .items(Joi.string().regex(uiCapabilitiesRegex)) + .required(), +}); + +const schema = Joi.object({ + id: Joi.string() + .regex(featurePrivilegePartRegex) + .invalid(...prohibitedFeatureIds) + .required(), + name: Joi.string().required(), + validLicenses: Joi.array().items(Joi.string().valid('basic', 'standard', 'gold', 'platinum')), + icon: Joi.string(), + description: Joi.string(), + navLinkId: Joi.string(), + app: Joi.array() + .items(Joi.string()) + .required(), + management: managementSchema, + catalogue: catalogueSchema, + privileges: Joi.object({ + all: privilegeSchema, + read: privilegeSchema, + }).required(), + privilegesTooltip: Joi.string(), + reserved: Joi.object({ + privilege: privilegeSchema.required(), + description: Joi.string().required(), + }), +}); + +export class FeatureRegistry { + private locked = false; + private features: Record = {}; + + public register(feature: FeatureWithAllOrReadPrivileges) { + if (this.locked) { + throw new Error(`Features are locked, can't register new features`); + } + + validateFeature(feature); + + if (feature.id in this.features) { + throw new Error(`Feature with id ${feature.id} is already registered.`); + } + + this.features[feature.id] = feature as Feature; + } + + public getAll(): Feature[] { + this.locked = true; + return cloneDeep(Object.values(this.features)); + } +} + +function validateFeature(feature: FeatureWithAllOrReadPrivileges) { + const validateResult = Joi.validate(feature, schema); + if (validateResult.error) { + throw validateResult.error; + } + // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. + const { app = [], management = {}, catalogue = [] } = feature; + + const privilegeEntries = [...Object.entries(feature.privileges)]; + if (feature.reserved) { + privilegeEntries.push(['reserved', feature.reserved.privilege]); + } + + privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => { + if (!privilegeDefinition) { + throw new Error('Privilege definition may not be null or undefined'); + } + + const unknownAppEntries = difference(privilegeDefinition.app || [], app); + if (unknownAppEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown app entries: ${unknownAppEntries.join(', ')}` + ); + } + + const unknownCatalogueEntries = difference(privilegeDefinition.catalogue || [], catalogue); + if (unknownCatalogueEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown catalogue entries: ${unknownCatalogueEntries.join(', ')}` + ); + } + + Object.entries(privilegeDefinition.management || {}).forEach( + ([managementSectionId, managementEntry]) => { + if (!management[managementSectionId]) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown management section: ${managementSectionId}` + ); + } + + const unknownSectionEntries = difference(managementEntry, management[managementSectionId]); + + if (unknownSectionEntries.length > 0) { + throw new Error( + `Feature privilege ${ + feature.id + }.${privilegeId} has unknown management entries for section ${managementSectionId}: ${unknownSectionEntries.join( + ', ' + )}` + ); + } + } + ); + }); +} diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/index.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/index.ts new file mode 100644 index 00000000000000..8cbff5df3b2b4d --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { + Feature, + FeatureKibanaPrivileges, + FeatureRegistry, + FeatureWithAllOrReadPrivileges, + uiCapabilitiesRegex, +} from './feature_registry'; diff --git a/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts new file mode 100644 index 00000000000000..7c08356e27b807 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.test.ts @@ -0,0 +1,302 @@ +/* + * 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 { UICapabilities } from 'ui/capabilities'; +import { Feature } from './feature_registry'; +import { populateUICapabilities } from './populate_ui_capabilities'; + +function getMockXpackMainPlugin(features: Feature[]) { + return { + getFeatures: () => features, + }; +} + +function getMockOriginalInjectedVars() { + return { + uiCapabilities: { + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + feature: { + someCapability: true, + }, + otherFeature: {}, + }, + }; +} + +function createFeaturePrivilege(key: string, capabilities: string[] = []) { + return { + [key]: { + savedObject: { + all: [], + read: [], + }, + app: [], + ui: [...capabilities], + }, + }; +} + +describe('populateUICapabilities', () => { + it('handles no original uiCapabilites and no registered features gracefully', () => { + const xpackMainPlugin = getMockXpackMainPlugin([]); + + expect(populateUICapabilities(xpackMainPlugin, {} as UICapabilities)).toEqual({}); + }); + + it('returns the original uiCapabilities untouched when no features are registered', () => { + const xpackMainPlugin = getMockXpackMainPlugin([]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + otherFeature: {}, + }); + }); + + it('handles features with no registered capabilities', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('all'), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: {}, + otherFeature: {}, + }); + }); + + it('augments the original uiCapabilities with registered feature capabilities', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('all', ['capability1', 'capability2']), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + }, + otherFeature: {}, + }); + }); + + it('combines catalogue entries from multiple features', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + catalogue: ['anotherFooEntry', 'anotherBarEntry'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + ...createFeaturePrivilege('baz'), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + anotherFooEntry: true, + barEntry: true, + anotherBarEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + otherFeature: {}, + }); + }); + + it(`merges capabilities from all feature privileges`, () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + ...createFeaturePrivilege('baz', ['capability1', 'capability5']), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + capability5: true, + }, + otherFeature: {}, + }); + }); + + it('supports merging multiple features with multiple privileges each', () => { + const xpackMainPlugin = getMockXpackMainPlugin([ + { + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + ...createFeaturePrivilege('baz', ['capability1', 'capability5']), + }, + }, + { + id: 'anotherNewFeature', + name: 'another new feature', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('foo', ['capability1', 'capability2']), + ...createFeaturePrivilege('bar', ['capability3', 'capability4']), + }, + }, + { + id: 'yetAnotherNewFeature', + name: 'yet another new feature', + navLinkId: 'yetAnotherNavLink', + app: ['bar-app'], + privileges: { + ...createFeaturePrivilege('all', ['capability1', 'capability2']), + ...createFeaturePrivilege('read', []), + ...createFeaturePrivilege('somethingInBetween', [ + 'something1', + 'something2', + 'something3', + ]), + }, + }, + ]); + const originalInjectedVars = getMockOriginalInjectedVars(); + + expect(populateUICapabilities(xpackMainPlugin, originalInjectedVars.uiCapabilities)).toEqual({ + anotherNewFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + feature: { + someCapability: true, + }, + navLinks: { + foo: true, + bar: true, + }, + management: {}, + catalogue: { + fooEntry: true, + barEntry: true, + }, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + capability5: true, + }, + otherFeature: {}, + yetAnotherNewFeature: { + capability1: true, + capability2: true, + something1: true, + something2: true, + something3: true, + }, + }); + }); +}); diff --git a/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts new file mode 100644 index 00000000000000..4d7548c38725fe --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/populate_ui_capabilities.ts @@ -0,0 +1,84 @@ +/* + * 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'; +import { Feature } from '../../types'; + +const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue']; + +interface FeatureCapabilities { + [featureId: string]: Record; +} + +export function populateUICapabilities( + xpackMainPlugin: Record, + uiCapabilities: UICapabilities +): UICapabilities { + const features: Feature[] = xpackMainPlugin.getFeatures(); + + const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature); + + return mergeCapabilities(uiCapabilities || {}, ...featureCapabilities); +} + +function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { + const UIFeatureCapabilities: FeatureCapabilities = { + catalogue: {}, + [feature.id]: {}, + }; + + if (feature.catalogue) { + UIFeatureCapabilities.catalogue = { + ...UIFeatureCapabilities.catalogue, + ...feature.catalogue.reduce( + (acc, capability) => ({ + ...acc, + [capability]: true, + }), + {} + ), + }; + } + + Object.values(feature.privileges).forEach(privilege => { + UIFeatureCapabilities[feature.id] = { + ...UIFeatureCapabilities[feature.id], + ...privilege.ui.reduce( + (privilegeAcc, capability) => ({ + ...privilegeAcc, + [capability]: true, + }), + {} + ), + }; + }); + + return UIFeatureCapabilities; +} + +function mergeCapabilities( + originalCapabilities: UICapabilities, + ...allFeatureCapabilities: FeatureCapabilities[] +): UICapabilities { + return allFeatureCapabilities.reduce((acc, capabilities) => { + const mergableCapabilities: UICapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); + + const mergedFeatureCapabilities = { + ...mergableCapabilities, + ...acc, + }; + + ELIGIBLE_FLAT_MERGE_KEYS.forEach(key => { + mergedFeatureCapabilities[key] = { + ...mergedFeatureCapabilities[key], + ...capabilities[key], + }; + }); + + return mergedFeatureCapabilities; + }, originalCapabilities); +} diff --git a/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts b/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts new file mode 100644 index 00000000000000..fa44808f18d537 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/lib/register_oss_features.ts @@ -0,0 +1,224 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { Feature } from './feature_registry'; + +const kibanaFeatures: Feature[] = [ + { + id: 'discover', + name: i18n.translate('xpack.main.featureRegistry.discoverFeatureName', { + defaultMessage: 'Discover', + }), + icon: 'discoverApp', + navLinkId: 'kibana:discover', + app: ['kibana'], + catalogue: ['discover'], + privileges: { + all: { + savedObject: { + all: ['search', 'url'], + read: ['config', 'index-pattern'], + }, + ui: ['show', 'createShortUrl', 'save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'search', 'url'], + }, + ui: ['show'], + }, + }, + }, + { + id: 'visualize', + name: i18n.translate('xpack.main.featureRegistry.visualizeFeatureName', { + defaultMessage: 'Visualize', + }), + icon: 'visualizeApp', + navLinkId: 'kibana:visualize', + app: ['kibana'], + catalogue: ['visualize'], + privileges: { + all: { + savedObject: { + all: ['visualization', 'url'], + read: ['config', 'index-pattern', 'search'], + }, + ui: ['show', 'createShortUrl', 'delete', 'save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'search', 'visualization'], + }, + ui: ['show'], + }, + }, + }, + { + id: 'dashboard', + name: i18n.translate('xpack.main.featureRegistry.dashboardFeatureName', { + defaultMessage: 'Dashboard', + }), + icon: 'dashboardApp', + navLinkId: 'kibana:dashboard', + app: ['kibana'], + catalogue: ['dashboard'], + privileges: { + all: { + savedObject: { + all: ['dashboard', 'url'], + read: [ + 'config', + 'index-pattern', + 'search', + 'visualization', + 'timelion-sheet', + 'canvas-workpad', + ], + }, + ui: ['createNew', 'show', 'showWriteControls'], + }, + read: { + savedObject: { + all: [], + read: [ + 'config', + 'index-pattern', + 'search', + 'visualization', + 'timelion-sheet', + 'canvas-workpad', + 'dashboard', + ], + }, + ui: ['show'], + }, + }, + }, + { + id: 'dev_tools', + name: i18n.translate('xpack.main.featureRegistry.devToolsFeatureName', { + defaultMessage: 'Dev Tools', + }), + icon: 'devToolsApp', + navLinkId: 'kibana:dev_tools', + app: ['kibana'], + catalogue: ['console', 'searchprofiler', 'grokdebugger'], + privileges: { + all: { + api: ['console'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + read: { + api: ['console'], + savedObject: { + all: [], + read: ['config'], + }, + ui: ['show'], + }, + }, + privilegesTooltip: i18n.translate('xpack.main.featureRegistry.devToolsPrivilegesTooltip', { + defaultMessage: + 'User should also be granted the appropriate Elasticsearch cluster and index privileges', + }), + }, + { + id: 'advancedSettings', + name: i18n.translate('xpack.main.featureRegistry.advancedSettingsFeatureName', { + defaultMessage: 'Advanced Settings', + }), + icon: 'advancedSettingsApp', + app: ['kibana'], + catalogue: ['advanced_settings'], + management: { + kibana: ['settings'], + }, + privileges: { + all: { + savedObject: { + all: ['config'], + read: [], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['config'], + }, + ui: [], + }, + }, + }, + { + id: 'indexPatterns', + name: i18n.translate('xpack.main.featureRegistry.indexPatternFeatureName', { + defaultMessage: 'Index Pattern Management', + }), + icon: 'indexPatternApp', + app: ['kibana'], + catalogue: ['index_patterns'], + management: { + kibana: ['index_patterns'], + }, + privileges: { + all: { + savedObject: { + all: ['index-pattern'], + read: ['config'], + }, + ui: ['createNew'], + }, + read: { + savedObject: { + all: [], + read: ['index-pattern', 'config'], + }, + ui: [], + }, + }, + }, +]; + +const timelionFeatures: Feature[] = [ + { + id: 'timelion', + name: 'Timelion', + icon: 'timelionApp', + navLinkId: 'timelion', + app: ['timelion', 'kibana'], + catalogue: ['timelion'], + privileges: { + all: { + savedObject: { + all: ['timelion-sheet'], + read: ['config', 'index-pattern'], + }, + ui: ['save'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'index-pattern', 'timelion-sheet'], + }, + ui: [], + }, + }, + }, +]; + +export function registerOssFeatures(registerFeature: (feature: Feature) => void) { + for (const feature of [...kibanaFeatures, ...timelionFeatures]) { + registerFeature(feature); + } +} diff --git a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js index 3f945fe962fdbf..2a1d34e0299e1c 100644 --- a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js +++ b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js @@ -5,15 +5,22 @@ */ import { getTelemetryOptIn } from './get_telemetry_opt_in'; -import { buildUserProfile } from './user_profile_registry'; +import { populateUICapabilities } from './populate_ui_capabilities'; export async function replaceInjectedVars(originalInjectedVars, request, server) { const xpackInfo = server.plugins.xpack_main.info; - const withXpackInfo = async () => ({ + + const originalInjectedVarsWithUICapabilities = { ...originalInjectedVars, + uiCapabilities: { + ...populateUICapabilities(server.plugins.xpack_main, originalInjectedVars.uiCapabilities), + } + }; + + const withXpackInfo = async () => ({ + ...originalInjectedVarsWithUICapabilities, telemetryOptedIn: await getTelemetryOptIn(request), xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined, - userProfile: await buildUserProfile(request), }); // security feature is disabled @@ -23,7 +30,7 @@ export async function replaceInjectedVars(originalInjectedVars, request, server) // not enough license info to make decision one way or another if (!xpackInfo.isAvailable() || !xpackInfo.feature('security').getLicenseCheckResults()) { - return originalInjectedVars; + return originalInjectedVarsWithUICapabilities; } // authentication is not a thing you can do @@ -33,7 +40,7 @@ export async function replaceInjectedVars(originalInjectedVars, request, server) // request is not authenticated if (!await server.plugins.security.isAuthenticated(request)) { - return originalInjectedVars; + return originalInjectedVarsWithUICapabilities; } // plugin enabled, license is appropriate, request is authenticated 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 125e481c384a16..bd93283739d939 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 @@ -6,6 +6,7 @@ import { injectXPackInfoSignature } from './inject_xpack_info_signature'; import { XPackInfo } from './xpack_info'; +import { FeatureRegistry } from './feature_registry'; /** * Setup the X-Pack Main plugin. This is fired every time that the Elasticsearch plugin becomes Green. @@ -24,6 +25,10 @@ export function setupXPackMain(server) { server.expose('createXPackInfo', (options) => new XPackInfo(server, options)); server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); + const featureRegistry = new FeatureRegistry(); + server.expose('registerFeature', (feature) => featureRegistry.register(feature)); + server.expose('getFeatures', () => featureRegistry.getAll()); + const setPluginStatus = () => { if (info.isAvailable()) { server.plugins.xpack_main.status.green('Ready'); diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts deleted file mode 100644 index 22a0b58b60c2ab..00000000000000 --- a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts +++ /dev/null @@ -1,36 +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 { - buildUserProfile, - registerUserProfileCapabilityFactory, - removeAllFactories, -} from './user_profile_registry'; - -describe('UserProfileRegistry', () => { - beforeEach(() => removeAllFactories()); - - it('should produce an empty user profile', async () => { - expect(await buildUserProfile(null)).toEqual({}); - }); - - it('should accumulate the results of all registered factories', async () => { - registerUserProfileCapabilityFactory(async () => ({ - foo: true, - bar: false, - })); - - registerUserProfileCapabilityFactory(async () => ({ - anotherCapability: true, - })); - - expect(await buildUserProfile(null)).toEqual({ - foo: true, - bar: false, - anotherCapability: true, - }); - }); -}); diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts deleted file mode 100644 index 417341165fde40..00000000000000 --- a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts +++ /dev/null @@ -1,32 +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. - */ - -export type CapabilityFactory = (request: any) => Promise<{ [capability: string]: boolean }>; - -let factories: CapabilityFactory[] = []; - -export function removeAllFactories() { - factories = []; -} - -export function registerUserProfileCapabilityFactory(factory: CapabilityFactory) { - factories.push(factory); -} - -export async function buildUserProfile(request: any) { - const factoryPromises = factories.map(async factory => ({ - ...(await factory(request)), - })); - - const factoryResults = await Promise.all(factoryPromises); - - return factoryResults.reduce((acc, capabilities) => { - return { - ...acc, - ...capabilities, - }; - }, {}); -} diff --git a/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts b/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts index 0c6b7b0eaafaa0..ab09e0d73b80da 100644 --- a/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts +++ b/x-pack/plugins/xpack_main/server/lib/xpack_info_license.d.ts @@ -12,7 +12,7 @@ export declare class XPackInfoLicense { public getUid(): string | undefined; public isActive(): boolean; public getExpiryDateInMillis(): number | undefined; - public isOneOf(candidateLicenses: string): boolean; + public isOneOf(candidateLicenses: string[]): boolean; public getType(): LicenseType | undefined; public getMode(): string | undefined; public isActiveLicense(typeChecker: (mode: string) => boolean): boolean; diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap b/x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap new file mode 100644 index 00000000000000..856ca089d878e1 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/__snapshots__/features.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GET /api/features/v1 does not return features that arent allowed by current license 1`] = ` +Array [ + Object { + "app": Array [], + "id": "feature_1", + "name": "Feature 1", + "privileges": Object {}, + }, +] +`; + +exports[`GET /api/features/v1 returns a list of available features 1`] = ` +Array [ + Object { + "app": Array [], + "id": "feature_1", + "name": "Feature 1", + "privileges": Object {}, + }, + Object { + "app": Array [ + "bar-app", + ], + "id": "licensed_feature", + "name": "Licensed Feature", + "privileges": Object {}, + "validLicenses": Array [ + "gold", + ], + }, +] +`; diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts new file mode 100644 index 00000000000000..c66f15b4698fbb --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { Server } from 'hapi'; +import { KibanaConfig } from 'src/legacy/server/kbn_server'; +import { FeatureRegistry } from '../../../../lib/feature_registry'; +// @ts-ignore +import { setupXPackMain } from '../../../../lib/setup_xpack_main'; +import { featuresRoute } from './features'; + +let server: Server; +let currentLicenseLevel: string = 'gold'; + +describe('GET /api/features/v1', () => { + beforeAll(() => { + server = new Server(); + + const config: Record = {}; + server.config = () => { + return { + get: (key: string) => { + return config[key]; + }, + } as KibanaConfig; + }; + const featureRegistry = new FeatureRegistry(); + // @ts-ignore + server.plugins.xpack_main = { + getFeatures: () => featureRegistry.getAll(), + info: { + // @ts-ignore + license: { + isOneOf: (candidateLicenses: string[]) => { + return candidateLicenses.includes(currentLicenseLevel); + }, + }, + }, + }; + + featuresRoute(server); + + featureRegistry.register({ + id: 'feature_1', + name: 'Feature 1', + app: [], + privileges: {}, + }); + + featureRegistry.register({ + id: 'licensed_feature', + name: 'Licensed Feature', + app: ['bar-app'], + validLicenses: ['gold'], + privileges: {}, + }); + }); + + it('returns a list of available features', async () => { + const response = await server.inject({ + url: '/api/features/v1', + }); + + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchSnapshot(); + }); + + it(`does not return features that arent allowed by current license`, async () => { + currentLicenseLevel = 'basic'; + + const response = await server.inject({ + url: '/api/features/v1', + }); + + expect(response.statusCode).toEqual(200); + expect(JSON.parse(response.payload)).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts new file mode 100644 index 00000000000000..f54c7990217f5c --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/features.ts @@ -0,0 +1,26 @@ +/* + * 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 { Feature } from '../../../../../types'; + +export function featuresRoute(server: Record) { + server.route({ + path: '/api/features/v1', + method: 'GET', + async handler(request: Record) { + const xpackInfo = server.plugins.xpack_main.info; + + const allFeatures: Feature[] = server.plugins.xpack_main.getFeatures(); + + return allFeatures.filter( + feature => + !feature.validLicenses || + !feature.validLicenses.length || + xpackInfo.license.isOneOf(feature.validLicenses) + ); + }, + }); +} diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/features/index.ts b/x-pack/plugins/xpack_main/server/routes/api/v1/features/index.ts new file mode 100644 index 00000000000000..856f0b583d7850 --- /dev/null +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/features/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { featuresRoute } from './features'; diff --git a/x-pack/plugins/xpack_main/server/routes/api/v1/index.js b/x-pack/plugins/xpack_main/server/routes/api/v1/index.js index 8148d6a7a883af..608ec38670e694 100644 --- a/x-pack/plugins/xpack_main/server/routes/api/v1/index.js +++ b/x-pack/plugins/xpack_main/server/routes/api/v1/index.js @@ -6,4 +6,5 @@ export { xpackInfoRoute } from './xpack_info'; export { telemetryRoute } from './telemetry'; +export { featuresRoute } from './features'; export { settingsRoute } from './settings'; diff --git a/x-pack/plugins/xpack_main/types.ts b/x-pack/plugins/xpack_main/types.ts new file mode 100644 index 00000000000000..61dd43970d008b --- /dev/null +++ b/x-pack/plugins/xpack_main/types.ts @@ -0,0 +1,11 @@ +/* + * 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 { + Feature, + FeatureKibanaPrivileges, + uiCapabilitiesRegex, +} from './server/lib/feature_registry'; diff --git a/x-pack/plugins/xpack_main/xpack_main.d.ts b/x-pack/plugins/xpack_main/xpack_main.d.ts index 2dac0da0c39226..8a2471352c5ff7 100644 --- a/x-pack/plugins/xpack_main/xpack_main.d.ts +++ b/x-pack/plugins/xpack_main/xpack_main.d.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { Feature, FeatureWithAllOrReadPrivileges } from './server/lib/feature_registry'; import { XPackInfo, XPackInfoOptions } from './server/lib/xpack_info'; +export { XPackFeature } from './server/lib/xpack_info'; export interface XPackMainPlugin { info: XPackInfo; createXPackInfo(options: XPackInfoOptions): XPackInfo; + getFeatures(): Feature[]; + registerFeature(feature: FeatureWithAllOrReadPrivileges): void; } diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 25c9e4808ebadc..a5a17411b7c718 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -18,5 +18,8 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/saved_object_api_integration/security_and_spaces/config'), require.resolve('../test/saved_object_api_integration/security_only/config'), require.resolve('../test/saved_object_api_integration/spaces_only/config'), + require.resolve('../test/ui_capabilities/security_and_spaces/config'), + require.resolve('../test/ui_capabilities/security_only/config'), + require.resolve('../test/ui_capabilities/spaces_only/config'), require.resolve('../test/upgrade_assistant_integration/config'), ]); diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts new file mode 100644 index 00000000000000..e96ac46b210b9c --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -0,0 +1,338 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + const log = getService('log'); + + const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); + const end = encodeURIComponent(new Date().toISOString()); + + const expect404 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 404); + }; + + const expect200 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + }; + + const endpoints = [ + { + url: `/api/apm/services/foo/errors?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/errors/bar?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/errors/bar/distribution?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'distribution' of undefined" + ); + }, + }, + { + url: `/api/apm/services/foo/errors/distribution?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'distribution' of undefined" + ); + }, + }, + { + url: `/api/apm/services/foo/metrics/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot destructure property `timeseriesData` of 'undefined' or 'null'." + ); + }, + }, + { + url: `/api/apm/services?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/traces?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/traces/foo?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'transactions' of undefined" + ); + }, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar/baz/charts?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + url: `/api/apm/services/foo/transaction_groups/bar/baz/distribution?start=${start}&end=${end}`, + expectForbidden: expect404, + expectResponse: (result: any) => { + expect(result.response).to.have.property('statusCode', 400); + expect(result.response.body).to.have.property( + 'message', + "Cannot read property 'stats' of undefined" + ); + }, + }, + ]; + + async function executeRequest( + endpoint: string, + username: string, + password: string, + spaceId?: string + ) { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return await supertest + .get(`${basePath}${endpoint}`) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + } + + async function executeRequests( + username: string, + password: string, + spaceId: string, + expectation: 'forbidden' | 'response' + ) { + for (const endpoint of endpoints) { + log.debug(`hitting ${endpoint}`); + const result = await executeRequest(endpoint.url, username, password, spaceId); + if (expectation === 'forbidden') { + endpoint.expectForbidden(result); + } else { + endpoint.expectResponse(result); + } + } + } + + describe('feature controls', () => { + it(`APIs can't be accessed by apm-* read privileges role`, async () => { + const username = 'logstash_read'; + const roleName = 'logstash_read'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await executeRequests(username, password, '', 'forbidden'); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('APIs can be accessed global all with apm-* read privileges role', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await executeRequests(username, password, '', 'response'); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the APM feature + it(`APIs can't be accessed by dashboard all with apm-* read privileges role`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await executeRequests(username, password, '', 'forbidden'); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has uptime read access to space_1 and dashboard all access to space_2 + const space1Id = 'space_1'; + const space2Id = 'space_2'; + + const roleName = 'user_1'; + const username = 'user_1'; + const password = 'user_1-password'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['apm-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: [space1Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space2Id], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await security.role.delete(roleName); + await security.user.delete(username); + }); + + it('user_1 can access APIs in space_1', async () => { + await executeRequests(username, password, space1Id, 'response'); + }); + + it(`user_1 can't access APIs in space_2`, async () => { + await executeRequests(username, password, space2Id, 'forbidden'); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts new file mode 100644 index 00000000000000..b51432f10d1e92 --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function apmApiIntegrationTests({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('APM', () => { + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/api_integration/apis/console/feature_controls.ts b/x-pack/test/api_integration/apis/console/feature_controls.ts new file mode 100644 index 00000000000000..615883d1fbbddd --- /dev/null +++ b/x-pack/test/api_integration/apis/console/feature_controls.ts @@ -0,0 +1,218 @@ +/* + * 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 { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function securityTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + + describe('/api/console/proxy', () => { + it('cannot be accessed by an anonymous user', async () => { + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .set('kbn-xsrf', 'xxx') + .send() + .expect(401); + }); + + it('can be accessed by kibana_user role', async () => { + const username = 'kibana_user'; + const roleName = 'kibana_user'; + try { + const password = `${username}-password`; + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.user.delete(username); + } + }); + + it('can be accessed by global all role', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + try { + const password = `${username}-password`; + + await security.role.create(roleName, { + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('can be accessed by global read role', async () => { + const username = 'global_read'; + const roleName = 'global_read'; + try { + const password = `${username}-password`; + + await security.role.create(roleName, { + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the dev_tools feature + it(`can't be accessed by a user with dashboard all access`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + try { + const password = `${username}-password`; + + await security.role.create(roleName, { + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + }); + + await supertest + .post(`/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(404); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has dev_tools all access to space_1 and dashboard access to space_2 + const space1Id = 'space_1'; + const user1 = { + username: 'user_1', + roleName: 'user_1', + password: 'user_1-password', + }; + + const space2Id = 'space_2'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await security.role.create(user1.roleName, { + kibana: [ + { + feature: { + dev_tools: ['all'], + }, + spaces: [space1Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space2Id], + }, + ], + }); + await security.user.create(user1.username, { + password: user1.password, + roles: [user1.roleName], + }); + + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await security.role.delete(user1.roleName); + await security.user.delete(user1.username); + }); + + it('user_1 can access dev_tools in space_1', async () => { + await supertest + .post(`/s/${space1Id}/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(user1.username, user1.password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(200); + }); + + it(`user_1 can't access dev_tools in space_2`, async () => { + await supertest + .post(`/s/${space2Id}/api/console/proxy?method=GET&path=${encodeURIComponent('/_cat')}`) + .auth(user1.username, user1.password) + .set('kbn-xsrf', 'xxx') + .send() + .expect(404); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/console/index.ts b/x-pack/test/api_integration/apis/console/index.ts new file mode 100644 index 00000000000000..512852a367beab --- /dev/null +++ b/x-pack/test/api_integration/apis/console/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function consoleApiIntegrationTests({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('console', () => { + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/api_integration/apis/index.js b/x-pack/test/api_integration/apis/index.js index 87ecedc1a2cfbc..8770ddcbd561c9 100644 --- a/x-pack/test/api_integration/apis/index.js +++ b/x-pack/test/api_integration/apis/index.js @@ -16,8 +16,10 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./kibana')); loadTestFile(require.resolve('./infra')); loadTestFile(require.resolve('./beats')); + loadTestFile(require.resolve('./console')); loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./uptime')); loadTestFile(require.resolve('./maps')); + loadTestFile(require.resolve('./apm')); }); } diff --git a/x-pack/test/api_integration/apis/infra/feature_controls.ts b/x-pack/test/api_integration/apis/infra/feature_controls.ts new file mode 100644 index 00000000000000..4f4899e86a01bc --- /dev/null +++ b/x-pack/test/api_integration/apis/infra/feature_controls.ts @@ -0,0 +1,297 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import gql from 'graphql-tag'; +import { SecurityService, SpacesService } from '../../../common/services'; +import { KbnTestProvider } from './types'; + +const introspectionQuery = gql` + query Schema { + __schema { + queryType { + name + } + } + } +`; + +const featureControlsTests: KbnTestProvider = ({ getService }) => { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + const clientFactory = getService('infraOpsGraphQLClientFactory'); + + const expectGraphQL404 = (result: any) => { + expect(result.response).to.be(undefined); + expect(result.error).not.to.be(undefined); + expect(result.error).to.have.property('networkError'); + expect(result.error.networkError).to.have.property('statusCode', 404); + }; + + const expectGraphQLResponse = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).to.have.property('data'); + expect(result.response.data).to.be.an('object'); + }; + + const expectGraphIQL404 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 404); + }; + + const expectGraphIQLResponse = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + }; + + const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { + const queryOptions = { + query: introspectionQuery, + }; + + const basePath = spaceId ? `/s/${spaceId}` : ''; + + const client = clientFactory({ username, password, basePath }); + let error; + let response; + try { + response = await client.query(queryOptions); + } catch (err) { + error = err; + } + return { + error, + response, + }; + }; + + const executeGraphIQLRequest = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return supertest + .get(`${basePath}/api/infra/graphql/graphiql`) + .auth(username, password) + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + describe('feature controls', () => { + it(`APIs can't be accessed by user with logstash-* "read" privileges`, async () => { + const username = 'logstash_read'; + const roleName = 'logstash_read'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectGraphQL404(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password); + expectGraphIQL404(graphQLIResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('APIs can be accessed user with global "all" and logstash-* "read" privileges', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectGraphQLResponse(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password); + expectGraphIQLResponse(graphQLIResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the infra feature + it(`APIs can't be accessed by user with dashboard "all" and logstash-* "read" privileges`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectGraphQL404(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password); + expectGraphIQL404(graphQLIResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has infrastructure read access to space_1, logs read access to space_2 and dashboard all access to space_3 + const space1Id = 'space_1'; + const space2Id = 'space_2'; + const space3Id = 'space_3'; + + const roleName = 'user_1'; + const username = 'user_1'; + const password = 'user_1-password'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space3Id, + name: space3Id, + disabledFeatures: [], + }); + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + }, + spaces: [space1Id], + }, + { + feature: { + logs: ['read'], + }, + spaces: [space2Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space3Id], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await spaces.delete(space3Id); + await security.role.delete(roleName); + await security.user.delete(username); + }); + + it('user_1 can access APIs in space_1', async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space1Id); + expectGraphQLResponse(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password, space1Id); + expectGraphIQLResponse(graphQLIResult); + }); + + it(`user_1 can access APIs in space_2`, async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space2Id); + expectGraphQLResponse(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password, space2Id); + expectGraphIQLResponse(graphQLIResult); + }); + + it(`user_1 can't access APIs in space_3`, async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space3Id); + expectGraphQL404(graphQLResult); + + const graphQLIResult = await executeGraphIQLRequest(username, password, space3Id); + expectGraphIQL404(graphQLIResult); + }); + }); + }); +}; + +// eslint-disable-next-line import/no-default-export +export default featureControlsTests; diff --git a/x-pack/test/api_integration/apis/infra/index.js b/x-pack/test/api_integration/apis/infra/index.js index a4c2faed5f5ce6..c959facced06a1 100644 --- a/x-pack/test/api_integration/apis/infra/index.js +++ b/x-pack/test/api_integration/apis/infra/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./sources')); loadTestFile(require.resolve('./waffle')); loadTestFile(require.resolve('./log_item')); + loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/api_integration/apis/infra/types.ts b/x-pack/test/api_integration/apis/infra/types.ts index 932bef5952698e..585f10117b6ce3 100644 --- a/x-pack/test/api_integration/apis/infra/types.ts +++ b/x-pack/test/api_integration/apis/infra/types.ts @@ -12,10 +12,19 @@ export interface EsArchiver { unload(name: string): void; } +interface InfraOpsGraphQLClientFactoryOptions { + username: string; + password: string; + basePath: string; +} + export interface KbnTestProviderOptions { getService(name: string): any; getService(name: 'esArchiver'): EsArchiver; getService(name: 'infraOpsGraphQLClient'): ApolloClient; + getService( + name: 'infraOpsGraphQLClientFactory' + ): (options: InfraOpsGraphQLClientFactoryOptions) => ApolloClient; } export type KbnTestProvider = (options: KbnTestProviderOptions) => void; diff --git a/x-pack/test/api_integration/apis/kibana/stats/stats.js b/x-pack/test/api_integration/apis/kibana/stats/stats.js index 48c720bb57ba9b..cd0cae4ed10514 100644 --- a/x-pack/test/api_integration/apis/kibana/stats/stats.js +++ b/x-pack/test/api_integration/apis/kibana/stats/stats.js @@ -9,17 +9,9 @@ import expect from '@kbn/expect'; export default function ({ getService }) { const supertestNoAuth = getService('supertestWithoutAuth'); const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); describe('/api/stats', () => { describe('operational stats and usage stats', () => { - before('load clusters archive', () => { - return esArchiver.load('discover'); - }); - - after('unload clusters archive', () => { - return esArchiver.unload('discover'); - }); describe('no auth', () => { it('should return 200 and stats for no extended', async () => { diff --git a/x-pack/test/api_integration/apis/security/index.js b/x-pack/test/api_integration/apis/security/index.js index ff3e5b33e832b7..571e6d9390ab2a 100644 --- a/x-pack/test/api_integration/apis/security/index.js +++ b/x-pack/test/api_integration/apis/security/index.js @@ -8,5 +8,6 @@ export default function ({ loadTestFile }) { describe('security', () => { loadTestFile(require.resolve('./basic_login')); loadTestFile(require.resolve('./roles')); + loadTestFile(require.resolve('./privileges')); }); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts new file mode 100644 index 00000000000000..6da0748e607d53 --- /dev/null +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -0,0 +1,1341 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + let version: string; + + describe('Privileges', () => { + before(async () => { + const versionService = getService('kibanaServer').version; + version = await versionService.get(); + }); + + // This test also functions as a sanity check for assigned privilege actions, to ensure that feature privileges are being granted in the way that developers expect. + // It could also be considered a huge maintenance burden, which will be largely alleviated once we can use jest's snapshotting abiliites + // For any poor soul which would prefer to just regenerate the following expected payload and look at the diff to ensure the proper changes + // were made, you can use the following bash command: + // + // curl -s -u elastic:changeme http://localhost:5620/api/security/privileges?includeActions=true 2>&1 | perl -pe 's/"(\w+:)(\d\.\d\.\d)([^"]*)"/`$1\${version}$3`/g' + // + describe('GET /api/security/privileges?includeActions=true', () => { + it('should return a privilege map with all known privileges with actions', async () => { + await supertest + .get('/api/security/privileges?includeActions=true') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200, { + features: { + discover: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:search/create`, + `saved_object:${version}:search/bulk_create`, + `saved_object:${version}:search/update`, + `saved_object:${version}:search/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/search/delete`, + `ui:${version}:savedObjectsManagement/search/edit`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:discover/show`, + `ui:${version}:discover/createShortUrl`, + `ui:${version}:discover/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:discover/show`, + ], + }, + visualize: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:visualization/create`, + `saved_object:${version}:visualization/bulk_create`, + `saved_object:${version}:visualization/update`, + `saved_object:${version}:visualization/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `ui:${version}:savedObjectsManagement/visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/edit`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:visualize/show`, + `ui:${version}:visualize/createShortUrl`, + `ui:${version}:visualize/delete`, + `ui:${version}:visualize/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + ], + }, + dashboard: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `saved_object:${version}:dashboard/create`, + `saved_object:${version}:dashboard/bulk_create`, + `saved_object:${version}:dashboard/update`, + `saved_object:${version}:dashboard/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/dashboard/delete`, + `ui:${version}:savedObjectsManagement/dashboard/edit`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:dashboard/createNew`, + `ui:${version}:dashboard/show`, + `ui:${version}:dashboard/showWriteControls`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:dashboard/show`, + ], + }, + dev_tools: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:console`, + `app:${version}:kibana`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:dev_tools/show`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:console`, + `app:${version}:kibana`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:dev_tools/show`, + ], + }, + advancedSettings: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:config/create`, + `saved_object:${version}:config/bulk_create`, + `saved_object:${version}:config/update`, + `saved_object:${version}:config/delete`, + `ui:${version}:savedObjectsManagement/config/delete`, + `ui:${version}:savedObjectsManagement/config/edit`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:advancedSettings/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + indexPatterns: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:index-pattern/create`, + `saved_object:${version}:index-pattern/bulk_create`, + `saved_object:${version}:index-pattern/update`, + `saved_object:${version}:index-pattern/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/edit`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:indexPatterns/createNew`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + timelion: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:timelion`, + `app:${version}:kibana`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:timelion-sheet/create`, + `saved_object:${version}:timelion-sheet/bulk_create`, + `saved_object:${version}:timelion-sheet/update`, + `saved_object:${version}:timelion-sheet/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/edit`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:timelion/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:timelion`, + `app:${version}:kibana`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + ], + }, + graph: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:graph`, + `app:${version}:kibana`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `saved_object:${version}:graph-workspace/create`, + `saved_object:${version}:graph-workspace/bulk_create`, + `saved_object:${version}:graph-workspace/update`, + `saved_object:${version}:graph-workspace/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/edit`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:graph/save`, + `ui:${version}:graph/delete`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:graph`, + `app:${version}:kibana`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + ], + }, + apm: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:apm`, + `app:${version}:apm`, + `app:${version}:kibana`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:apm/show`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:apm`, + `app:${version}:apm`, + `app:${version}:kibana`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:apm/show`, + ], + }, + maps: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:maps`, + `app:${version}:kibana`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:map/create`, + `saved_object:${version}:map/bulk_create`, + `saved_object:${version}:map/update`, + `saved_object:${version}:map/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/map/delete`, + `ui:${version}:savedObjectsManagement/map/edit`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:maps/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:maps`, + `app:${version}:kibana`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + ], + }, + canvas: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:canvas`, + `app:${version}:kibana`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:canvas-workpad/create`, + `saved_object:${version}:canvas-workpad/bulk_create`, + `saved_object:${version}:canvas-workpad/update`, + `saved_object:${version}:canvas-workpad/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:canvas/save`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:canvas`, + `app:${version}:kibana`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + ], + }, + infrastructure: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:infrastructure/configureSource`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + ], + }, + logs: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:logs/show`, + `ui:${version}:logs/configureSource`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:infra`, + `app:${version}:infra`, + `app:${version}:kibana`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:logs/show`, + ], + }, + uptime: { + all: [ + 'login:', + `version:${version}`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `app:${version}:kibana`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `app:${version}:kibana`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + }, + global: { + all: [ + 'login:', + `version:${version}`, + `space:${version}:manage`, + `ui:${version}:spaces/manage`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:search/create`, + `saved_object:${version}:search/bulk_create`, + `saved_object:${version}:search/update`, + `saved_object:${version}:search/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/search/delete`, + `ui:${version}:savedObjectsManagement/search/edit`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:discover/show`, + `ui:${version}:discover/createShortUrl`, + `ui:${version}:discover/save`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:visualization/create`, + `saved_object:${version}:visualization/bulk_create`, + `saved_object:${version}:visualization/update`, + `saved_object:${version}:visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/edit`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:visualize/createShortUrl`, + `ui:${version}:visualize/delete`, + `ui:${version}:visualize/save`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `saved_object:${version}:dashboard/create`, + `saved_object:${version}:dashboard/bulk_create`, + `saved_object:${version}:dashboard/update`, + `saved_object:${version}:dashboard/delete`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/dashboard/delete`, + `ui:${version}:savedObjectsManagement/dashboard/edit`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:dashboard/createNew`, + `ui:${version}:dashboard/show`, + `ui:${version}:dashboard/showWriteControls`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/create`, + `saved_object:${version}:config/bulk_create`, + `saved_object:${version}:config/update`, + `saved_object:${version}:config/delete`, + `ui:${version}:savedObjectsManagement/config/delete`, + `ui:${version}:savedObjectsManagement/config/edit`, + `ui:${version}:advancedSettings/save`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/create`, + `saved_object:${version}:index-pattern/bulk_create`, + `saved_object:${version}:index-pattern/update`, + `saved_object:${version}:index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/edit`, + `ui:${version}:indexPatterns/createNew`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:timelion-sheet/create`, + `saved_object:${version}:timelion-sheet/bulk_create`, + `saved_object:${version}:timelion-sheet/update`, + `saved_object:${version}:timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/edit`, + `ui:${version}:timelion/save`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `saved_object:${version}:graph-workspace/create`, + `saved_object:${version}:graph-workspace/bulk_create`, + `saved_object:${version}:graph-workspace/update`, + `saved_object:${version}:graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/edit`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `ui:${version}:graph/save`, + `ui:${version}:graph/delete`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:map/create`, + `saved_object:${version}:map/bulk_create`, + `saved_object:${version}:map/update`, + `saved_object:${version}:map/delete`, + `ui:${version}:savedObjectsManagement/map/delete`, + `ui:${version}:savedObjectsManagement/map/edit`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:maps/save`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-workpad/create`, + `saved_object:${version}:canvas-workpad/bulk_create`, + `saved_object:${version}:canvas-workpad/update`, + `saved_object:${version}:canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:canvas/save`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:infrastructure/configureSource`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `ui:${version}:logs/configureSource`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:discover/show`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:dashboard/show`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `ui:${version}:savedObjectsManagement/map/read`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + ], + }, + space: { + all: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:search/create`, + `saved_object:${version}:search/bulk_create`, + `saved_object:${version}:search/update`, + `saved_object:${version}:search/delete`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `saved_object:${version}:url/create`, + `saved_object:${version}:url/bulk_create`, + `saved_object:${version}:url/update`, + `saved_object:${version}:url/delete`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `ui:${version}:savedObjectsManagement/search/delete`, + `ui:${version}:savedObjectsManagement/search/edit`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/delete`, + `ui:${version}:savedObjectsManagement/url/edit`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:discover/show`, + `ui:${version}:discover/createShortUrl`, + `ui:${version}:discover/save`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `saved_object:${version}:visualization/create`, + `saved_object:${version}:visualization/bulk_create`, + `saved_object:${version}:visualization/update`, + `saved_object:${version}:visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/delete`, + `ui:${version}:savedObjectsManagement/visualization/edit`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:visualize/createShortUrl`, + `ui:${version}:visualize/delete`, + `ui:${version}:visualize/save`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `saved_object:${version}:dashboard/create`, + `saved_object:${version}:dashboard/bulk_create`, + `saved_object:${version}:dashboard/update`, + `saved_object:${version}:dashboard/delete`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `ui:${version}:savedObjectsManagement/dashboard/delete`, + `ui:${version}:savedObjectsManagement/dashboard/edit`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:dashboard/createNew`, + `ui:${version}:dashboard/show`, + `ui:${version}:dashboard/showWriteControls`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `saved_object:${version}:config/create`, + `saved_object:${version}:config/bulk_create`, + `saved_object:${version}:config/update`, + `saved_object:${version}:config/delete`, + `ui:${version}:savedObjectsManagement/config/delete`, + `ui:${version}:savedObjectsManagement/config/edit`, + `ui:${version}:advancedSettings/save`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `saved_object:${version}:index-pattern/create`, + `saved_object:${version}:index-pattern/bulk_create`, + `saved_object:${version}:index-pattern/update`, + `saved_object:${version}:index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/delete`, + `ui:${version}:savedObjectsManagement/index-pattern/edit`, + `ui:${version}:indexPatterns/createNew`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `saved_object:${version}:timelion-sheet/create`, + `saved_object:${version}:timelion-sheet/bulk_create`, + `saved_object:${version}:timelion-sheet/update`, + `saved_object:${version}:timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/delete`, + `ui:${version}:savedObjectsManagement/timelion-sheet/edit`, + `ui:${version}:timelion/save`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `saved_object:${version}:graph-workspace/create`, + `saved_object:${version}:graph-workspace/bulk_create`, + `saved_object:${version}:graph-workspace/update`, + `saved_object:${version}:graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/delete`, + `ui:${version}:savedObjectsManagement/graph-workspace/edit`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `ui:${version}:graph/save`, + `ui:${version}:graph/delete`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `saved_object:${version}:map/create`, + `saved_object:${version}:map/bulk_create`, + `saved_object:${version}:map/update`, + `saved_object:${version}:map/delete`, + `ui:${version}:savedObjectsManagement/map/delete`, + `ui:${version}:savedObjectsManagement/map/edit`, + `ui:${version}:savedObjectsManagement/map/read`, + `ui:${version}:maps/save`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `saved_object:${version}:canvas-workpad/create`, + `saved_object:${version}:canvas-workpad/bulk_create`, + `saved_object:${version}:canvas-workpad/update`, + `saved_object:${version}:canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/delete`, + `ui:${version}:savedObjectsManagement/canvas-workpad/edit`, + `ui:${version}:canvas/save`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `saved_object:${version}:infrastructure-ui-source/create`, + `saved_object:${version}:infrastructure-ui-source/bulk_create`, + `saved_object:${version}:infrastructure-ui-source/update`, + `saved_object:${version}:infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/delete`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/edit`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:infrastructure/configureSource`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `ui:${version}:logs/configureSource`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + 'allHack:', + ], + read: [ + 'login:', + `version:${version}`, + `app:${version}:kibana`, + `ui:${version}:catalogue/discover`, + `ui:${version}:navLinks/kibana:discover`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `saved_object:${version}:index-pattern/bulk_get`, + `saved_object:${version}:index-pattern/get`, + `saved_object:${version}:index-pattern/find`, + `saved_object:${version}:search/bulk_get`, + `saved_object:${version}:search/get`, + `saved_object:${version}:search/find`, + `saved_object:${version}:url/bulk_get`, + `saved_object:${version}:url/get`, + `saved_object:${version}:url/find`, + `ui:${version}:savedObjectsManagement/config/read`, + `ui:${version}:savedObjectsManagement/index-pattern/read`, + `ui:${version}:savedObjectsManagement/search/read`, + `ui:${version}:savedObjectsManagement/url/read`, + `ui:${version}:discover/show`, + `ui:${version}:catalogue/visualize`, + `ui:${version}:navLinks/kibana:visualize`, + `saved_object:${version}:visualization/bulk_get`, + `saved_object:${version}:visualization/get`, + `saved_object:${version}:visualization/find`, + `ui:${version}:savedObjectsManagement/visualization/read`, + `ui:${version}:visualize/show`, + `ui:${version}:catalogue/dashboard`, + `ui:${version}:navLinks/kibana:dashboard`, + `saved_object:${version}:timelion-sheet/bulk_get`, + `saved_object:${version}:timelion-sheet/get`, + `saved_object:${version}:timelion-sheet/find`, + `saved_object:${version}:canvas-workpad/bulk_get`, + `saved_object:${version}:canvas-workpad/get`, + `saved_object:${version}:canvas-workpad/find`, + `saved_object:${version}:dashboard/bulk_get`, + `saved_object:${version}:dashboard/get`, + `saved_object:${version}:dashboard/find`, + `ui:${version}:savedObjectsManagement/timelion-sheet/read`, + `ui:${version}:savedObjectsManagement/canvas-workpad/read`, + `ui:${version}:savedObjectsManagement/dashboard/read`, + `ui:${version}:dashboard/show`, + `api:${version}:console`, + `ui:${version}:catalogue/console`, + `ui:${version}:catalogue/searchprofiler`, + `ui:${version}:catalogue/grokdebugger`, + `ui:${version}:navLinks/kibana:dev_tools`, + `ui:${version}:dev_tools/show`, + `ui:${version}:catalogue/advanced_settings`, + `ui:${version}:management/kibana/settings`, + `ui:${version}:catalogue/index_patterns`, + `ui:${version}:management/kibana/index_patterns`, + `app:${version}:timelion`, + `ui:${version}:catalogue/timelion`, + `ui:${version}:navLinks/timelion`, + `app:${version}:graph`, + `ui:${version}:catalogue/graph`, + `ui:${version}:navLinks/graph`, + `saved_object:${version}:graph-workspace/bulk_get`, + `saved_object:${version}:graph-workspace/get`, + `saved_object:${version}:graph-workspace/find`, + `ui:${version}:savedObjectsManagement/graph-workspace/read`, + `api:${version}:apm`, + `app:${version}:apm`, + `ui:${version}:catalogue/apm`, + `ui:${version}:navLinks/apm`, + `ui:${version}:apm/show`, + `app:${version}:maps`, + `ui:${version}:catalogue/maps`, + `ui:${version}:navLinks/maps`, + `saved_object:${version}:map/bulk_get`, + `saved_object:${version}:map/get`, + `saved_object:${version}:map/find`, + `ui:${version}:savedObjectsManagement/map/read`, + `app:${version}:canvas`, + `ui:${version}:catalogue/canvas`, + `ui:${version}:navLinks/canvas`, + `api:${version}:infra`, + `app:${version}:infra`, + `ui:${version}:catalogue/infraops`, + `ui:${version}:navLinks/infra:home`, + `saved_object:${version}:infrastructure-ui-source/bulk_get`, + `saved_object:${version}:infrastructure-ui-source/get`, + `saved_object:${version}:infrastructure-ui-source/find`, + `ui:${version}:savedObjectsManagement/infrastructure-ui-source/read`, + `ui:${version}:infrastructure/show`, + `ui:${version}:catalogue/infralogging`, + `ui:${version}:navLinks/infra:logs`, + `ui:${version}:logs/show`, + `api:${version}:uptime`, + `app:${version}:uptime`, + `ui:${version}:catalogue/uptime`, + `ui:${version}:navLinks/uptime`, + ], + }, + reserved: { + monitoring: [ + `version:${version}`, + `app:${version}:monitoring`, + `app:${version}:kibana`, + `ui:${version}:catalogue/monitoring`, + `ui:${version}:navLinks/monitoring`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + ml: [ + `version:${version}`, + `app:${version}:ml`, + `app:${version}:kibana`, + `ui:${version}:catalogue/ml`, + `ui:${version}:navLinks/ml`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, + }); + }); + + describe('GET /api/security/privileges', () => { + it('should return a privilege map with all known privileges, without actions', async () => { + await supertest + .get('/api/security/privileges') + .set('kbn-xsrf', 'xxx') + .send() + .expect(200, { + features: { + discover: ['all', 'read'], + visualize: ['all', 'read'], + dashboard: ['all', 'read'], + dev_tools: ['all', 'read'], + advancedSettings: ['all', 'read'], + indexPatterns: ['all', 'read'], + timelion: ['all', 'read'], + graph: ['all', 'read'], + maps: ['all', 'read'], + canvas: ['all', 'read'], + infrastructure: ['all', 'read'], + logs: ['all', 'read'], + uptime: ['all', 'read'], + apm: ['all', 'read'], + }, + global: ['all', 'read'], + space: ['all', 'read'], + reserved: ['monitoring', 'ml'], + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/security/roles.js b/x-pack/test/api_integration/apis/security/roles.js index 46eb56ab5602e2..5e5c2f3d3b188f 100644 --- a/x-pack/test/api_integration/apis/security/roles.js +++ b/x-pack/test/api_integration/apis/security/roles.js @@ -41,10 +41,24 @@ export default function ({ getService }) { ], run_as: ['watcher_user'], }, - kibana: { - global: ['all', 'read'], - space: {} - } + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + } + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ] }) .expect(204); @@ -67,8 +81,13 @@ export default function ({ getService }) { applications: [ { application: 'kibana-.kibana', - privileges: ['all', 'read'], + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], resources: ['*'], + }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], } ], run_as: ['watcher_user'], @@ -141,10 +160,25 @@ export default function ({ getService }) { ], run_as: ['watcher_user'], }, - kibana: { - global: ['all', 'read'], - space: {} - } + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + }, + spaces: ['*'] + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ], }) .expect(204); @@ -167,9 +201,14 @@ export default function ({ getService }) { applications: [ { application: 'kibana-.kibana', - privileges: ['all', 'read'], + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], resources: ['*'], }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], + }, { application: 'logstash-default', privileges: ['logstash-privilege'], @@ -188,6 +227,100 @@ export default function ({ getService }) { }); }); + describe('Get Role', async () => { + it('should get roles', async () => { + await es.shield.putRole({ + name: 'role_to_get', + body: { + cluster: ['manage'], + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + field_security: { + grant: ['*'], + except: ['geo.*'] + }, + query: `{ "match": { "geo.src": "CN" } }`, + }, + ], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], + resources: ['*'], + }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], + }, + { + application: 'logstash-default', + privileges: ['logstash-privilege'], + resources: ['*'], + }, + ], + run_as: ['watcher_user'], + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { + enabled: true, + }, + } + }); + + await supertest.get('/api/security/role/role_to_get') + .set('kbn-xsrf', 'xxx') + .expect(200, { + name: 'role_to_get', + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { enabled: true }, + elasticsearch: { + cluster: ['manage'], + indices: [ + { + field_security: { + grant: ['*'], + except: ['geo.*'] + }, + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + query: `{ "match": { "geo.src": "CN" } }`, + allow_restricted_indices: false + }, + ], + run_as: ['watcher_user'], + }, + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + }, + spaces: ['*'] + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ], + + _transform_error: [], + _unrecognized_applications: [ 'logstash-default' ] + }); + }); + }); describe('Delete Role', () => { it('should delete the three roles we created', async () => { await supertest.delete('/api/security/role/empty_role').set('kbn-xsrf', 'xxx').expect(204); diff --git a/x-pack/test/api_integration/apis/uptime/constants.ts b/x-pack/test/api_integration/apis/uptime/constants.ts new file mode 100644 index 00000000000000..664a2a467df71b --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/constants.ts @@ -0,0 +1,10 @@ +/* + * 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 moment from 'moment'; + +export const PINGS_DATE_RANGE_START = moment('2018-10-30T00:00:23.889Z').valueOf(); +export const PINGS_DATE_RANGE_END = moment('2018-10-31T00:00:00.889Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/uptime/feature_controls.ts b/x-pack/test/api_integration/apis/uptime/feature_controls.ts new file mode 100644 index 00000000000000..243e5ebdddd90d --- /dev/null +++ b/x-pack/test/api_integration/apis/uptime/feature_controls.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { docCountQueryString } from '../../../../plugins/uptime/public/queries'; +import { SecurityService, SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { PINGS_DATE_RANGE_END, PINGS_DATE_RANGE_START } from './constants'; + +// eslint-disable-next-line import/no-default-export +export default function featureControlsTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertestWithoutAuth'); + const security: SecurityService = getService('security'); + const spaces: SpacesService = getService('spaces'); + + const expect404 = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 404); + }; + + const expectResponse = (result: any) => { + expect(result.error).to.be(undefined); + expect(result.response).not.to.be(undefined); + expect(result.response).to.have.property('statusCode', 200); + }; + + const executeGraphQLQuery = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + const getDocCountQuery = { + operationName: null, + query: docCountQueryString, + variables: {}, + }; + + return await supertest + .post(`${basePath}/api/uptime/graphql`) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .send({ ...getDocCountQuery }) + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + const executeIsValidRequest = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return await supertest + .get(`${basePath}/api/uptime/is_valid`) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + const executePingsRequest = async (username: string, password: string, spaceId?: string) => { + const basePath = spaceId ? `/s/${spaceId}` : ''; + + return await supertest + .get( + `${basePath}/api/uptime/pings?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` + ) + .auth(username, password) + .set('kbn-xsrf', 'foo') + .then((response: any) => ({ error: undefined, response })) + .catch((error: any) => ({ error, response: undefined })); + }; + + describe('feature controls', () => { + it(`APIs can't be accessed by heartbeat-* read privileges role`, async () => { + const username = 'logstash_read'; + const roleName = 'logstash_read'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expect404(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expect404(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expect404(pingsResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + it('APIs can be accessed global all with heartbeat-* read privileges role', async () => { + const username = 'global_all'; + const roleName = 'global_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expectResponse(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expectResponse(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expectResponse(pingsResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + // this could be any role which doesn't have access to the uptime feature + it(`APIs can't be accessed by dashboard all with heartbeat-* read privileges role`, async () => { + const username = 'dashboard_all'; + const roleName = 'dashboard_all'; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const graphQLResult = await executeGraphQLQuery(username, password); + expect404(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expect404(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expect404(pingsResult); + } finally { + await security.role.delete(roleName); + await security.user.delete(username); + } + }); + + describe('spaces', () => { + // the following tests create a user_1 which has uptime read access to space_1 and dashboard all access to space_2 + const space1Id = 'space_1'; + const space2Id = 'space_2'; + + const roleName = 'user_1'; + const username = 'user_1'; + const password = 'user_1-password'; + + before(async () => { + await spaces.create({ + id: space1Id, + name: space1Id, + disabledFeatures: [], + }); + await spaces.create({ + id: space2Id, + name: space2Id, + disabledFeatures: [], + }); + await security.role.create(roleName, { + elasticsearch: { + indices: [ + { + names: ['heartbeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + uptime: ['read'], + }, + spaces: [space1Id], + }, + { + feature: { + dashboard: ['all'], + }, + spaces: [space2Id], + }, + ], + }); + await security.user.create(username, { + password, + roles: [roleName], + }); + }); + + after(async () => { + await spaces.delete(space1Id); + await spaces.delete(space2Id); + await security.role.delete(roleName); + await security.user.delete(username); + }); + + it('user_1 can access APIs in space_1', async () => { + const graphQLResult = await executeGraphQLQuery(username, password, space1Id); + expectResponse(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password, space1Id); + expectResponse(isValidResult); + + const pingsResult = await executePingsRequest(username, password, space1Id); + expectResponse(pingsResult); + }); + + it(`user_1 can't access APIs in space_2`, async () => { + const graphQLResult = await executeGraphQLQuery(username, password); + expect404(graphQLResult); + + const isValidResult = await executeIsValidRequest(username, password); + expect404(isValidResult); + + const pingsResult = await executePingsRequest(username, password); + expect404(pingsResult); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/uptime/get_all_pings.js b/x-pack/test/api_integration/apis/uptime/get_all_pings.js index 6bdbad28c438d8..f336b528291683 100644 --- a/x-pack/test/api_integration/apis/uptime/get_all_pings.js +++ b/x-pack/test/api_integration/apis/uptime/get_all_pings.js @@ -6,12 +6,11 @@ import moment from 'moment'; import expect from '@kbn/expect'; +import { PINGS_DATE_RANGE_START, PINGS_DATE_RANGE_END } from './constants'; export default function ({ getService }) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - let dateRangeStart = moment('2018-10-30T00:00:23.889Z').valueOf(); - let dateRangeEnd = moment('2018-10-31T00:00:00.889Z').valueOf(); describe('get_all_pings', () => { const archive = 'uptime/pings'; @@ -21,7 +20,7 @@ export default function ({ getService }) { it('should get all pings stored in index', async () => { const { body: apiResponse } = await supertest - .get(`/api/uptime/pings?sort=desc&dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`) + .get(`/api/uptime/pings?sort=desc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}`) .expect(200); expect(apiResponse.total).to.be(2); @@ -32,7 +31,7 @@ export default function ({ getService }) { it('should sort pings according to timestamp', async () => { const { body: apiResponse } = await supertest .get( - `/api/uptime/pings?sort=asc&dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + `/api/uptime/pings?sort=asc&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` ) .expect(200); @@ -45,7 +44,7 @@ export default function ({ getService }) { it('should return results of n length', async () => { const { body: apiResponse } = await supertest .get( - `/api/uptime/pings?sort=desc&size=1&dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}` + `/api/uptime/pings?sort=desc&size=1&dateRangeStart=${PINGS_DATE_RANGE_START}&dateRangeEnd=${PINGS_DATE_RANGE_END}` ) .expect(200); @@ -55,8 +54,8 @@ export default function ({ getService }) { }); it('should miss pings outside of date range', async () => { - dateRangeStart = moment('2002-01-01').valueOf(); - dateRangeEnd = moment('2002-01-02').valueOf(); + const dateRangeStart = moment('2002-01-01').valueOf(); + const dateRangeEnd = moment('2002-01-02').valueOf(); const { body: apiResponse } = await supertest .get(`/api/uptime/pings?dateRangeStart=${dateRangeStart}&dateRangeEnd=${dateRangeEnd}`) .expect(200); diff --git a/x-pack/test/api_integration/apis/uptime/index.js b/x-pack/test/api_integration/apis/uptime/index.js index cde44bd4f56ddc..79cfed312765db 100644 --- a/x-pack/test/api_integration/apis/uptime/index.js +++ b/x-pack/test/api_integration/apis/uptime/index.js @@ -14,6 +14,7 @@ export default function ({ getService, loadTestFile }) { ignore: [404], })); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./get_all_pings')); loadTestFile(require.resolve('./graphql')); }); diff --git a/x-pack/test/api_integration/apis/xpack_main/features/features.ts b/x-pack/test/api_integration/apis/xpack_main/features/features.ts new file mode 100644 index 00000000000000..265ce5f475839c --- /dev/null +++ b/x-pack/test/api_integration/apis/xpack_main/features/features.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { Feature } from '../../../../../plugins/xpack_main/types'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: KibanaFunctionalTestDefaultProviders) { + const supertest = getService('supertest'); + + describe('/api/features', () => { + describe('with trial license', () => { + it('should return a full feature set', async () => { + const { body } = await supertest + .get('/api/features/v1') + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body).to.be.an(Array); + + const featureIds = body.map((b: Feature) => b.id); + expect(featureIds.sort()).to.eql( + [ + 'discover', + 'visualize', + 'dashboard', + 'dev_tools', + 'advancedSettings', + 'indexPatterns', + 'timelion', + 'graph', + 'monitoring', + 'ml', + 'apm', + 'canvas', + 'infrastructure', + 'logs', + 'maps', + 'uptime', + ].sort() + ); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/xpack_main/features/index.ts b/x-pack/test/api_integration/apis/xpack_main/features/index.ts new file mode 100644 index 00000000000000..fdf7abc9cb6225 --- /dev/null +++ b/x-pack/test/api_integration/apis/xpack_main/features/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Features', () => { + loadTestFile(require.resolve('./features')); + }); +} diff --git a/x-pack/test/api_integration/apis/xpack_main/index.js b/x-pack/test/api_integration/apis/xpack_main/index.js index b8404d7015e7b6..3c73bdfc6ec63d 100644 --- a/x-pack/test/api_integration/apis/xpack_main/index.js +++ b/x-pack/test/api_integration/apis/xpack_main/index.js @@ -6,6 +6,7 @@ export default function ({ loadTestFile }) { describe('xpack_main', () => { + loadTestFile(require.resolve('./features')); loadTestFile(require.resolve('./telemetry')); loadTestFile(require.resolve('./settings')); }); diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 934d50c7cce619..a9da26722c823b 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -9,9 +9,15 @@ import { EsSupertestWithoutAuthProvider, SupertestWithoutAuthProvider, UsageAPIProvider, - InfraOpsGraphQLProvider + InfraOpsGraphQLClientProvider, + InfraOpsGraphQLClientFactoryProvider, } from './services'; +import { + SecurityServiceProvider, + SpacesServiceProvider, +} from '../common/services'; + export default async function ({ readConfigFile }) { const kibanaAPITestsConfig = await readConfigFile(require.resolve('../../../test/api_integration/config.js')); @@ -26,12 +32,15 @@ export default async function ({ readConfigFile }) { esSupertest: kibanaAPITestsConfig.get('services.esSupertest'), supertestWithoutAuth: SupertestWithoutAuthProvider, esSupertestWithoutAuth: EsSupertestWithoutAuthProvider, - infraOpsGraphQLClient: InfraOpsGraphQLProvider, + infraOpsGraphQLClient: InfraOpsGraphQLClientProvider, + infraOpsGraphQLClientFactory: InfraOpsGraphQLClientFactoryProvider, es: EsProvider, esArchiver: kibanaCommonConfig.get('services.esArchiver'), usageAPI: UsageAPIProvider, kibanaServer: kibanaCommonConfig.get('services.kibanaServer'), chance: kibanaAPITestsConfig.get('services.chance'), + security: SecurityServiceProvider, + spaces: SpacesServiceProvider, }, esArchiver: xPackFunctionalTestsConfig.get('esArchiver'), junit: { diff --git a/x-pack/test/api_integration/services/index.js b/x-pack/test/api_integration/services/index.js index 96d349f8486295..87325c3b4ad917 100644 --- a/x-pack/test/api_integration/services/index.js +++ b/x-pack/test/api_integration/services/index.js @@ -8,4 +8,4 @@ export { EsProvider } from './es'; export { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth'; export { SupertestWithoutAuthProvider } from './supertest_without_auth'; export { UsageAPIProvider } from './usage_api'; -export { InfraOpsGraphQLProvider } from './infraops_graphql_client'; +export { InfraOpsGraphQLClientProvider, InfraOpsGraphQLClientFactoryProvider } from './infraops_graphql_client'; diff --git a/x-pack/test/api_integration/services/infraops_graphql_client.js b/x-pack/test/api_integration/services/infraops_graphql_client.js index 81cdc442f4fbdb..52f064ee9e1955 100644 --- a/x-pack/test/api_integration/services/infraops_graphql_client.js +++ b/x-pack/test/api_integration/services/infraops_graphql_client.js @@ -12,23 +12,34 @@ import { HttpLink } from 'apollo-link-http'; import introspectionQueryResultData from '../../../plugins/infra/public/graphql/introspection.json'; -export function InfraOpsGraphQLProvider({ getService }) { +export function InfraOpsGraphQLClientProvider({ getService }) { + return new InfraOpsGraphQLClientFactoryProvider({ getService })(); +} + +export function InfraOpsGraphQLClientFactoryProvider({ getService }) { const config = getService('config'); - const kbnURL = formatUrl(config.get('servers.kibana')); + const [superUsername, superPassword] = config.get('servers.elasticsearch.auth').split(':'); - return new ApolloClient({ - cache: new InMemoryCache({ - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }), - link: new HttpLink({ + return function ({ username = superUsername, password = superPassword, basePath = null } = {}) { + const kbnURLWithoutAuth = formatUrl({ ...config.get('servers.kibana'), auth: false }); + + const httpLink = new HttpLink({ credentials: 'same-origin', fetch, headers: { 'kbn-xsrf': 'xxx', + authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`, }, - uri: `${kbnURL}/api/infra/graphql`, - }), - }); + uri: `${kbnURLWithoutAuth}${basePath || ''}/api/infra/graphql`, + }); + + return new ApolloClient({ + cache: new InMemoryCache({ + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }), + link: httpLink, + }); + }; } diff --git a/x-pack/test/common/services/index.ts b/x-pack/test/common/services/index.ts new file mode 100644 index 00000000000000..37b588064856bc --- /dev/null +++ b/x-pack/test/common/services/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { SecurityServiceProvider, SecurityService } from './security'; +export { SpacesServiceProvider, SpacesService } from './spaces'; diff --git a/x-pack/test/common/services/security/index.ts b/x-pack/test/common/services/security/index.ts new file mode 100644 index 00000000000000..e5845098f23102 --- /dev/null +++ b/x-pack/test/common/services/security/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { format as formatUrl } from 'url'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; +import { Role } from './role'; +import { User } from './user'; + +export class SecurityService { + public role: Role; + public user: User; + + constructor(url: string, log: LogService) { + this.role = new Role(url, log); + this.user = new User(url, log); + } +} + +export function SecurityServiceProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new SecurityService(url, log); +} diff --git a/x-pack/test/common/services/security/role.ts b/x-pack/test/common/services/security/role.ts new file mode 100644 index 00000000000000..e96f85c702337e --- /dev/null +++ b/x-pack/test/common/services/security/role.ts @@ -0,0 +1,47 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { LogService } from '../../../types/services'; + +export class Role { + private log: LogService; + private axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/role' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(name: string, role: any) { + this.log.debug(`creating role ${name}`); + const { data, status, statusText } = await this.axios.put(`/api/security/role/${name}`, role); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`created role ${name}`); + } + + public async delete(name: string) { + this.log.debug(`deleting role ${name}`); + const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`); + if (status !== 204 && status !== 404) { + throw new Error( + `Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect( + data + )}` + ); + } + this.log.debug(`deleted role ${name}`); + } +} diff --git a/x-pack/test/common/services/security/user.ts b/x-pack/test/common/services/security/user.ts new file mode 100644 index 00000000000000..2685222150df98 --- /dev/null +++ b/x-pack/test/common/services/security/user.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 axios, { AxiosInstance } from 'axios'; +import util from 'util'; +import { LogService } from '../../../types/services'; + +export class User { + private log: LogService; + private axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/security/user' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(username: string, user: any) { + this.log.debug(`creating user ${username}`); + const { data, status, statusText } = await this.axios.post( + `/api/security/v1/users/${username}`, + { + username, + ...user, + } + ); + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`created user ${username}`); + } + + public async delete(username: string) { + this.log.debug(`deleting user ${username}`); + const { data, status, statusText } = await this.axios.delete( + `/api/security/v1/users/${username}` + ); + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`deleted user ${username}`); + } +} diff --git a/x-pack/test/common/services/spaces/index.ts b/x-pack/test/common/services/spaces/index.ts new file mode 100644 index 00000000000000..08d7a676e79190 --- /dev/null +++ b/x-pack/test/common/services/spaces/index.ts @@ -0,0 +1,56 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import { format as formatUrl } from 'url'; +import util from 'util'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; + +export class SpacesService { + private log: LogService; + private axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/spaces/space' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we do our own validation below and throw better error messages + }); + } + + public async create(space: any) { + this.log.debug('creating space'); + const { data, status, statusText } = await this.axios.post('/api/spaces/space', space); + + if (status !== 200) { + throw new Error( + `Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug('created space'); + } + + public async delete(spaceId: string) { + this.log.debug(`deleting space: ${spaceId}`); + const { data, status, statusText } = await this.axios.delete(`/api/spaces/space/${spaceId}`); + + if (status !== 204) { + throw new Error( + `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + ); + } + this.log.debug(`deleted space: ${spaceId}`); + } +} + +export function SpacesServiceProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + return new SpacesService(url, log); +} diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts new file mode 100644 index 00000000000000..1d345eea703701 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('security feature controls', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global advanced_settings all privileges', () => { + before(async () => { + await security.role.create('global_advanced_settings_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + advancedSettings: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_advanced_settings_all_user', { + password: 'global_advanced_settings_all_user-password', + roles: ['global_advanced_settings_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_advanced_settings_all_user', + 'global_advanced_settings_all_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_advanced_settings_all_role'), + security.user.delete('global_advanced_settings_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`allows settings to be changed`, async () => { + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'America/Phoenix'); + const advancedSetting = await PageObjects.settings.getAdvancedSettings('dateFormat:tz'); + expect(advancedSetting).to.be('America/Phoenix'); + }); + }); + + describe('global advanced_settings read-only privileges', () => { + before(async () => { + await security.role.create('global_advanced_settings_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + advancedSettings: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_advanced_settings_read_user', { + password: 'global_advanced_settings_read_user-password', + roles: ['global_advanced_settings_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_advanced_settings_read_user', + 'global_advanced_settings_read_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await security.role.delete('global_advanced_settings_read_role'); + await security.user.delete('global_advanced_settings_read_user'); + }); + + it('shows Management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`does not allow settings to be changed`, async () => { + await PageObjects.settings.clickKibanaSettings(); + await PageObjects.settings.expectDisabledAdvancedSetting('dateFormat:tz'); + }); + }); + + describe('no advanced_settings privileges', () => { + before(async () => { + await security.role.create('no_advanced_settings_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_advanced_settings_privileges_user', { + password: 'no_advanced_settings_privileges_user-password', + roles: ['no_advanced_settings_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_advanced_settings_privileges_user', + 'no_advanced_settings_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_advanced_settings_privileges_role'); + await security.user.delete('no_advanced_settings_privileges_user'); + }); + + it('shows Management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`does not allow navigation to advanced settings; redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts new file mode 100644 index 00000000000000..6b32bfd1874794 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Management navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Management'); + }); + + it(`allows settings to be changed`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.settings.setAdvancedSettingsSelect('dateFormat:tz', 'America/Phoenix'); + const advancedSetting = await PageObjects.settings.getAdvancedSettings('dateFormat:tz'); + expect(advancedSetting).to.be('America/Phoenix'); + }); + }); + + describe('space with Advanced Settings disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['advancedSettings'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/settings', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/advanced_settings/index.ts b/x-pack/test/functional/apps/advanced_settings/index.ts new file mode 100644 index 00000000000000..43bd8e01579136 --- /dev/null +++ b/x-pack/test/functional/apps/advanced_settings/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function advancedSettingsApp({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Advanced Settings', function canvasAppTestSuite() { + this.tags('ciGroup2'); // CI requires tags ヽ(゜Q。)ノ? + loadTestFile(require.resolve('./feature_controls/advanced_settings_security')); + loadTestFile(require.resolve('./feature_controls/advanced_settings_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts new file mode 100644 index 00000000000000..ee97fd1806d243 --- /dev/null +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_security.ts @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'error', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global apm all privileges', () => { + before(async () => { + await security.role.create('global_apm_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + apm: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_apm_all_user', { + password: 'global_apm_all_user-password', + roles: ['global_apm_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login('global_apm_all_user', 'global_apm_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('global_apm_all_role'); + await security.user.delete('global_apm_all_user'); + }); + + it('shows apm navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'APM', + 'Management', + ]); + }); + + it('can navigate to APM app', async () => { + await PageObjects.common.navigateToApp('apm'); + await testSubjects.existOrFail('apmMainContainer', 10000); + }); + }); + + describe('global apm read-only privileges', () => { + before(async () => { + await security.role.create('global_apm_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + apm: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_apm_read_user', { + password: 'global_apm_read_user-password', + roles: ['global_apm_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login('global_apm_read_user', 'global_apm_read_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await security.role.delete('global_apm_read_role'); + await security.user.delete('global_apm_read_user'); + }); + + it('shows apm navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['APM', 'Management']); + }); + + it('can navigate to APM app', async () => { + await PageObjects.common.navigateToApp('apm'); + await testSubjects.existOrFail('apmMainContainer', 10000); + }); + }); + + describe('no apm privileges', () => { + before(async () => { + await security.role.create('no_apm_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_apm_privileges_user', { + password: 'no_apm_privileges_user-password', + roles: ['no_apm_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_apm_privileges_user', + 'no_apm_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_apm_privileges_role'); + await security.user.delete('no_apm_privileges_user'); + }); + + it(`doesn't show APM navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('APM'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('apm', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts new file mode 100644 index 00000000000000..ff32bcc5c6c869 --- /dev/null +++ b/x-pack/test/functional/apps/apm/feature_controls/apm_spaces.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows apm navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('APM'); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('apm'); + await testSubjects.existOrFail('apmMainContainer', 10000); + }); + }); + + describe('space with Uptime disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['apm'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show apm navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('APM'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('apm', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/apm/feature_controls/index.ts b/x-pack/test/functional/apps/apm/feature_controls/index.ts new file mode 100644 index 00000000000000..29f87d9c6a078a --- /dev/null +++ b/x-pack/test/functional/apps/apm/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./apm_security')); + loadTestFile(require.resolve('./apm_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts new file mode 100644 index 00000000000000..d8f4177741113d --- /dev/null +++ b/x-pack/test/functional/apps/apm/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('APM', function() { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts new file mode 100644 index 00000000000000..7a934b5dcd8b90 --- /dev/null +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + describe('security feature controls', () => { + before(async () => { + await esArchiver.load('canvas/default'); + }); + + after(async () => { + await esArchiver.unload('canvas/default'); + }); + + describe('global canvas all privileges', () => { + before(async () => { + await security.role.create('global_canvas_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + canvas: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_canvas_all_user', { + password: 'global_canvas_all_user-password', + roles: ['global_canvas_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_canvas_all_user', + 'global_canvas_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_canvas_all_role'), + security.user.delete('global_canvas_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows canvas navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Canvas', 'Management']); + }); + + it(`landing page shows "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.canvas.expectCreateWorkpadButtonEnabled(); + }); + + it(`allows a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: true, + showLoginIfPrompted: false, + }); + + await PageObjects.canvas.expectAddElementButton(); + }); + + it(`allows a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + showLoginIfPrompted: false, + } + ); + + await PageObjects.canvas.expectAddElementButton(); + }); + }); + + describe('global canvas read-only privileges', () => { + before(async () => { + await security.role.create('global_canvas_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + canvas: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_canvas_read_user', { + password: 'global_canvas_read_user-password', + roles: ['global_canvas_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_canvas_read_user', + 'global_canvas_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_canvas_read_role'); + await security.user.delete('global_canvas_read_user'); + }); + + it('shows canvas navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Canvas', 'Management']); + }); + + it(`landing page shows disabled "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.canvas.expectCreateWorkpadButtonDisabled(); + }); + + it(`does not allow a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + // expect redirection to canvas landing + await PageObjects.canvas.expectCreateWorkpadButtonDisabled(); + }); + + it(`does not allow a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + } + ); + + await PageObjects.canvas.expectNoAddElementButton(); + }); + }); + + describe('no canvas privileges', () => { + before(async () => { + await security.role.create('no_canvas_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_canvas_privileges_user', { + password: 'no_canvas_privileges_user-password', + roles: ['no_canvas_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_canvas_privileges_user', + 'no_canvas_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_canvas_privileges_role'); + await security.user.delete('no_canvas_privileges_user'); + }); + + const getMessageText = async () => + await (await find.byCssSelector('body>pre')).getVisibleText(); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`create new workpad returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts new file mode 100644 index 00000000000000..eba2a5c507c9d8 --- /dev/null +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + describe('spaces feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('canvas/default'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('canvas/default'); + }); + + it('shows canvas navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Canvas'); + }); + + it(`landing page shows "Create new workpad" button`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.canvas.expectCreateWorkpadButtonEnabled(); + }); + + it(`allows a workpad to be created`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + + await PageObjects.canvas.expectAddElementButton(); + }); + + it(`allows a workpad to be edited`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + } + ); + + await PageObjects.canvas.expectAddElementButton(); + }); + }); + + describe('space with Canvas disabled', () => { + const getMessageText = async () => + await (await find.byCssSelector('body>pre')).getVisibleText(); + + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('spaces/disabled_features'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['canvas'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('spaces/disabled_features'); + }); + + it(`doesn't show canvas navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Canvas'); + }); + + it(`create new workpad returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`edit workpad returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl( + 'canvas', + 'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31', + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/canvas/index.js b/x-pack/test/functional/apps/canvas/index.js index 6620ee6c26f026..bc33161cc4e970 100644 --- a/x-pack/test/functional/apps/canvas/index.js +++ b/x-pack/test/functional/apps/canvas/index.js @@ -8,5 +8,7 @@ export default function canvasApp({ loadTestFile }) { describe('Canvas app', function canvasAppTestSuite() { this.tags('ciGroup2'); // CI requires tags ヽ(゜Q。)ノ? loadTestFile(require.resolve('./smoke_test')); + loadTestFile(require.resolve('./feature_controls/canvas_security')); + loadTestFile(require.resolve('./feature_controls/canvas_spaces')); }); } diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts new file mode 100644 index 00000000000000..0f04a41d885185 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -0,0 +1,342 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +// eslint-disable-next-line max-len +import { + createDashboardEditUrl, + DashboardConstants, +} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector', 'share']); + const appsMenu = getService('appsMenu'); + const panelActions = getService('dashboardPanelActions'); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('dashboard/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('dashboard/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global dashboard all privileges', () => { + before(async () => { + await security.role.create('global_dashboard_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_all_user', { + password: 'global_dashboard_all_user-password', + roles: ['global_dashboard_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_all_user', + 'global_dashboard_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_all_role'); + await security.user.delete('global_dashboard_all_user'); + }); + + it('shows dashboard navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Dashboard', + 'Management', + ]); + }); + + it(`landing page shows "Create new Dashboard" button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('dashboardLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`create new dashboard shows addNew button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('emptyDashboardAddPanelButton', 10000); + }); + + it(`can view existing Dashboard`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + }); + + it(`does not allow a visualization to be edited`, async () => { + await PageObjects.dashboard.gotoDashboardEditMode('A Dashboard'); + await panelActions.openContextMenu(); + await panelActions.expectMissingEditPanelAction(); + }); + + it(`Permalinks shows create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + }); + + describe('global dashboard & visualize all privileges', () => { + before(async () => { + await security.role.create('global_dashboard_visualize_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + visualize: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_visualize_all_user', { + password: 'global_dashboard_visualize_all_user-password', + roles: ['global_dashboard_visualize_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_visualize_all_user', + 'global_dashboard_visualize_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_visualize_all_role'); + await security.user.delete('global_dashboard_visualize_all_user'); + }); + + it(`allows a visualization to be edited`, async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('A Dashboard'); + await panelActions.openContextMenu(); + await panelActions.expectExistsEditPanelAction(); + }); + }); + + describe('global dashboard read-only privileges', () => { + before(async () => { + await security.role.create('global_dashboard_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dashboard_read_user', { + password: 'global_dashboard_read_user-password', + roles: ['global_dashboard_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dashboard_read_user', + 'global_dashboard_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dashboard_read_role'); + await security.user.delete('global_dashboard_read_user'); + }); + + it('shows dashboard navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Dashboard', 'Management']); + }); + + it(`landing page doesn't show "Create new Dashboard" button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('dashboardLandingPage', 10000); + await testSubjects.missingOrFail('newItemButton'); + }); + + it(`create new dashboard redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 20000); + }); + + it(`can view existing Dashboard`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + }); + + describe('no dashboard privileges', () => { + before(async () => { + await security.role.create('no_dashboard_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_dashboard_privileges_user', { + password: 'no_dashboard_privileges_user-password', + roles: ['no_dashboard_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_dashboard_privileges_user', + 'no_dashboard_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_dashboard_privileges_role'); + await security.user.delete('no_dashboard_privileges_user'); + }); + + it(`doesn't show dashboard navLink`, async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((navLink: any) => navLink.text)).to.not.contain(['Dashboard']); + }); + + it(`landing page redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`create new dashboard redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 20000); + }); + + it(`edit dashboard for object which doesn't exist redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + createDashboardEditUrl('i-dont-exist'), + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit dashboard for object which exists redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts new file mode 100644 index 00000000000000..227dd43da36fcd --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_spaces.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +// eslint-disable-next-line max-len +import { + createDashboardEditUrl, + DashboardConstants, +} from '../../../../../../src/legacy/core_plugins/kibana/public/dashboard/dashboard_constants'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('spaces', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('dashboard/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('dashboard/feature_controls/spaces'); + }); + + it('shows dashboard navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Dashboard'); + }); + + it(`landing page shows "Create new Dashboard" button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.LANDING_PAGE_PATH, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('dashboardLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`create new dashboard shows addNew button`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('emptyDashboardAddPanelButton', 10000); + }); + + it(`can view existing Dashboard`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('dashboardPanelHeading-APie', 10000); + }); + }); + + describe('space with Dashboard disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('dashboard/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['dashboard'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('dashboard/feature_controls/spaces'); + }); + + it(`doesn't show dashboard navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Dashboard'); + }); + + it(`create new dashboard redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + DashboardConstants.CREATE_NEW_DASHBOARD_URL, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit dashboard for object which doesn't exist redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + createDashboardEditUrl('i-dont-exist'), + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit dashboard for object which exists redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', createDashboardEditUrl('i-exist'), { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/index.ts b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts new file mode 100644 index 00000000000000..37179d4e1dc7e2 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./dashboard_security')); + loadTestFile(require.resolve('./dashboard_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts new file mode 100644 index 00000000000000..5cea2fda9644d6 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('dashboard', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js index 6b7a20ad333da8..4b3f11dbf2c4f4 100644 --- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js +++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js @@ -25,6 +25,7 @@ export default function ({ getService, getPageObjects }) { 'header', 'settings', 'timePicker', + 'share', ]); const dashboardName = 'Dashboard View Mode Test Dashboard'; const savedSearchName = 'Saved search for dashboard'; @@ -165,9 +166,14 @@ export default function ({ getService, getPageObjects }) { expect(reportingMenuItemExists).to.be(false); }); - it('does not show the sharing menu item', async () => { + it('shows the sharing menu item', async () => { const shareMenuItemExists = await testSubjects.exists('shareTopNavButton'); - expect(shareMenuItemExists).to.be(false); + expect(shareMenuItemExists).to.be(true); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); }); it('does not show the visualization edit icon', async () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts new file mode 100644 index 00000000000000..d564b680905993 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'console', 'security']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const grokDebugger = getService('grokDebugger'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global dev_tools all privileges', () => { + before(async () => { + await security.role.create('global_dev_tools_all_role', { + kibana: [ + { + feature: { + dev_tools: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dev_tools_all_user', { + password: 'global_dev_tools_all_user-password', + roles: ['global_dev_tools_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dev_tools_all_user', + 'global_dev_tools_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dev_tools_all_role'); + await security.user.delete('global_dev_tools_all_user'); + }); + + it('shows Dev Tools navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Dev Tools', + 'Management', + ]); + }); + + it(`can navigate to console`, async () => { + await PageObjects.common.navigateToApp('console'); + await testSubjects.existOrFail('console'); + }); + + it(`can navigate to search profiler`, async () => { + await PageObjects.common.navigateToApp('searchProfiler'); + await testSubjects.existOrFail('searchProfiler'); + }); + + it(`can navigate to grok debugger`, async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + }); + + describe('global dev_tools read-only privileges', () => { + before(async () => { + await security.role.create('global_dev_tools_read_role', { + kibana: [ + { + feature: { + dev_tools: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_dev_tools_read_user', { + password: 'global_dev_tools_read_user-password', + roles: ['global_dev_tools_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_dev_tools_read_user', + 'global_dev_tools_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_dev_tools_read_role'); + await security.user.delete('global_dev_tools_read_user'); + }); + + it(`shows 'Dev Tools' navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Dev Tools', 'Management']); + }); + + it(`can navigate to console`, async () => { + await PageObjects.common.navigateToApp('console'); + await testSubjects.existOrFail('console'); + }); + + it(`can navigate to search profiler`, async () => { + await PageObjects.common.navigateToApp('searchProfiler'); + await testSubjects.existOrFail('searchProfiler'); + }); + + it(`can navigate to grok debugger`, async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + }); + + describe('no dev_tools privileges', () => { + before(async () => { + await security.role.create('no_dev_tools_privileges_role', { + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_dev_tools_privileges_user', { + password: 'no_dev_tools_privileges_user-password', + roles: ['no_dev_tools_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_dev_tools_privileges_user', + 'no_dev_tools_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_dev_tools_privileges_role'); + await security.user.delete('no_dev_tools_privileges_user'); + }); + + it(`doesn't show 'Dev Tools' navLink`, async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((navLink: any) => navLink.text)).to.not.contain(['Dev Tools']); + }); + + it(`navigating to console redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('console', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to search profiler redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('searchProfiler', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to grok debugger redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('grokDebugger', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts new file mode 100644 index 00000000000000..c4a16c18d3beb0 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_spaces.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'spaceSelector']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const grokDebugger = getService('grokDebugger'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`shows 'Dev Tools' navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Dev Tools'); + }); + + it(`can navigate to console`, async () => { + await PageObjects.common.navigateToApp('console'); + await testSubjects.existOrFail('console'); + }); + + it(`can navigate to search profiler`, async () => { + await PageObjects.common.navigateToApp('searchProfiler'); + await testSubjects.existOrFail('searchProfiler'); + }); + + it(`can navigate to grok debugger`, async () => { + await PageObjects.common.navigateToApp('grokDebugger'); + await grokDebugger.assertExists(); + }); + }); + + describe('space with dev_tools disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['dev_tools'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show 'Dev Tools' navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Dev Tools'); + }); + + it(`navigating to console redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('console', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to search profiler redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('searchProfiler', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`navigating to grok debugger redirect to homepage`, async () => { + await PageObjects.common.navigateToUrl('grokDebugger', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts new file mode 100644 index 00000000000000..d1c53dbc9c43fd --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./dev_tools_security')); + loadTestFile(require.resolve('./dev_tools_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/dev_tools/index.ts b/x-pack/test/functional/apps/dev_tools/index.ts new file mode 100644 index 00000000000000..04135bf27a54d8 --- /dev/null +++ b/x-pack/test/functional/apps/dev_tools/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('console', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts new file mode 100644 index 00000000000000..0afb925ee46f99 --- /dev/null +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -0,0 +1,253 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'security', + 'share', + 'spaceSelector', + ]); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + async function setDiscoverTimeRange() { + const fromTime = '2015-09-19 06:31:44.000'; + const toTime = '2015-09-23 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + + describe('security', () => { + before(async () => { + await esArchiver.load('discover/feature_controls/security'); + await esArchiver.loadIfNeeded('logstash_functional'); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + }); + + after(async () => { + await esArchiver.unload('discover/feature_controls/security'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.logout(); + }); + + describe('global discover all privileges', () => { + before(async () => { + await security.role.create('global_discover_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_discover_all_user', { + password: 'global_discover_all_user-password', + roles: ['global_discover_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_discover_all_user', + 'global_discover_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_discover_all_role'); + await security.user.delete('global_discover_all_user'); + }); + + it('shows discover navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Discover', + 'Management', + ]); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.existOrFail('discoverSaveButton', 20000); + }); + + it('Permalinks shows create short-url button', async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + }); + + describe('global discover read-only privileges', () => { + before(async () => { + await security.role.create('global_discover_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_discover_read_user', { + password: 'global_discover_read_user-password', + roles: ['global_discover_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_discover_read_user', + 'global_discover_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_discover_read_role'); + await security.user.delete('global_discover_read_user'); + }); + + it('shows discover navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`doesn't show save button`, async () => { + await PageObjects.common.navigateToApp('discover'); + await testSubjects.existOrFail('discoverNewButton', 10000); + await testSubjects.missingOrFail('discoverSaveButton'); + }); + + it(`doesn't show visualize button`, async () => { + await PageObjects.common.navigateToApp('discover'); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectMissingFieldListItemVisualize('bytes'); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + }); + + describe('discover and visualize privileges', () => { + before(async () => { + await security.role.create('global_discover_visualize_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['read'], + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_discover_visualize_read_user', { + password: 'global_discover_visualize_read_user-password', + roles: ['global_discover_visualize_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_discover_visualize_read_user', + 'global_discover_visualize_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_discover_visualize_read_role'); + await security.user.delete('global_discover_visualize_read_user'); + }); + + it(`shows the visualize button`, async () => { + await PageObjects.common.navigateToApp('discover'); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectFieldListItemVisualize('bytes'); + }); + }); + + describe('no discover privileges', () => { + before(async () => { + await security.role.create('no_discover_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_discover_privileges_user', { + password: 'no_discover_privileges_user-password', + roles: ['no_discover_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_discover_privileges_user', + 'no_discover_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_discover_privileges_role'); + await security.user.delete('no_discover_privileges_user'); + }); + + it(`redirects to the home page`, async () => { + await PageObjects.common.navigateToUrl('discover', '', { + ensureCurrentUrl: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts new file mode 100644 index 00000000000000..7fffbcfd103466 --- /dev/null +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects([ + 'common', + 'discover', + 'timePicker', + 'security', + 'spaceSelector', + ]); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + async function setDiscoverTimeRange() { + const fromTime = '2015-09-19 06:31:44.000'; + const toTime = '2015-09-23 18:31:44.000'; + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + } + + describe('spaces', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('discover/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('discover/feature_controls/spaces'); + }); + + it('shows discover navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Discover'); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('discover', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('discoverSaveButton', 10000); + }); + + it('shows "visualize" field button', async () => { + await PageObjects.common.navigateToApp('discover', { + basePath: '/s/custom_space', + }); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectFieldListItemVisualize('bytes'); + }); + }); + + describe('space with Discover disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('discover/feature_controls/spaces'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['discover'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('discover/feature_controls/spaces'); + }); + + it(`doesn't show discover navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Discover'); + }); + + it(`redirects to the home page`, async () => { + // to test whether they're being redirected properly, we first load + // the discover app in the default space, and then we load up the discover + // app in the custom space and ensure we end up on the home page + await PageObjects.common.navigateToApp('discover'); + await PageObjects.common.navigateToUrl('discover', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + await PageObjects.spaceSelector.expectHomePage('custom_space'); + }); + }); + + describe('space with Visualize disabled', async () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('spaces/disabled_features'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['visualize'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('spaces/disabled_features'); + }); + + it('Does not show the "visualize" field button', async () => { + await PageObjects.common.navigateToApp('discover', { + basePath: '/s/custom_space', + }); + await setDiscoverTimeRange(); + await PageObjects.discover.clickFieldListItem('bytes'); + await PageObjects.discover.expectMissingFieldListItemVisualize('bytes'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/discover/feature_controls/index.ts b/x-pack/test/functional/apps/discover/feature_controls/index.ts new file mode 100644 index 00000000000000..1f822c8c799846 --- /dev/null +++ b/x-pack/test/functional/apps/discover/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./discover_security')); + loadTestFile(require.resolve('./discover_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts new file mode 100644 index 00000000000000..575d485e6115a8 --- /dev/null +++ b/x-pack/test/functional/apps/discover/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('discover', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts new file mode 100644 index 00000000000000..4adb27bb3b188c --- /dev/null +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -0,0 +1,194 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global graph all privileges', () => { + before(async () => { + await security.role.create('global_graph_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + graph: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_graph_all_user', { + password: 'global_graph_all_user-password', + roles: ['global_graph_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_graph_all_user', + 'global_graph_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_graph_all_role'); + await security.user.delete('global_graph_all_user'); + }); + + it('shows graph navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Graph', + 'Management', + ]); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphSaveButton'); + }); + + it('shows delete button', async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphDeleteButton'); + }); + }); + + describe('global graph read-only privileges', () => { + before(async () => { + await security.role.create('global_graph_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + graph: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_graph_read_user', { + password: 'global_graph_read_user-password', + roles: ['global_graph_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_graph_read_user', + 'global_graph_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_graph_read_role'); + await security.user.delete('global_graph_read_user'); + }); + + it('shows graph navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Graph', 'Management']); + }); + + it(`doesn't show save button`, async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphOpenButton'); + await testSubjects.missingOrFail('graphSaveButton'); + }); + + it(`doesn't show delete button`, async () => { + await PageObjects.common.navigateToApp('graph'); + await testSubjects.existOrFail('graphOpenButton'); + await testSubjects.missingOrFail('graphDeleteButton'); + }); + }); + + describe('no graph privileges', () => { + before(async () => { + await security.role.create('no_graph_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_graph_privileges_user', { + password: 'no_graph_privileges_user-password', + roles: ['no_graph_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_graph_privileges_user', + 'no_graph_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_graph_privileges_role'); + await security.user.delete('no_graph_privileges_user'); + }); + + it(`doesn't show graph navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Graph'); + }); + + it(`navigating to app displays a 404`, async () => { + await PageObjects.common.navigateToUrl('graph', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts new file mode 100644 index 00000000000000..976286fefc3910 --- /dev/null +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_spaces.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'graph', 'security', 'error']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows graph navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Graph'); + }); + + it('shows save button', async () => { + await PageObjects.common.navigateToApp('graph', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('graphSaveButton'); + }); + + it('shows delete button', async () => { + await PageObjects.common.navigateToApp('graph', { + basePath: '/s/custom_space', + }); + await testSubjects.existOrFail('graphDeleteButton'); + }); + }); + + describe('space with Graph disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['graph'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show graph navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Graph'); + }); + + it(`navigating to app shows 404`, async () => { + await PageObjects.common.navigateToUrl('graph', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/graph/feature_controls/index.ts b/x-pack/test/functional/apps/graph/feature_controls/index.ts new file mode 100644 index 00000000000000..95efaf58b969b3 --- /dev/null +++ b/x-pack/test/functional/apps/graph/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./graph_security')); + loadTestFile(require.resolve('./graph_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/graph/index.js b/x-pack/test/functional/apps/graph/index.js index 98b360320c2c79..1eafe341ba2dbb 100644 --- a/x-pack/test/functional/apps/graph/index.js +++ b/x-pack/test/functional/apps/graph/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('graph app', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./graph')); }); } diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts new file mode 100644 index 00000000000000..2d039817315328 --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./index_patterns_security')); + loadTestFile(require.resolve('./index_patterns_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts new file mode 100644 index 00000000000000..dccbde316a1f00 --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global index_patterns all privileges', () => { + before(async () => { + await security.role.create('global_index_patterns_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + indexPatterns: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_index_patterns_all_user', { + password: 'global_index_patterns_all_user-password', + roles: ['global_index_patterns_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_index_patterns_all_user', + 'global_index_patterns_all_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_index_patterns_all_role'), + security.user.delete('global_index_patterns_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`index pattern listing shows create button`, async () => { + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('createIndexPatternButton'); + }); + }); + + describe('global index_patterns read-only privileges', () => { + before(async () => { + await security.role.create('global_index_patterns_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + indexPatterns: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_index_patterns_read_user', { + password: 'global_index_patterns_read_user-password', + roles: ['global_index_patterns_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_index_patterns_read_user', + 'global_index_patterns_read_user-password', + { + expectSpaceSelector: false, + } + ); + + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + }); + + after(async () => { + await security.role.delete('global_index_patterns_read_role'); + await security.user.delete('global_index_patterns_read_user'); + }); + + it('shows management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Management']); + }); + + it(`index pattern listing doesn't show create button`, async () => { + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('indexPatternTable'); + await testSubjects.missingOrFail('createIndexPatternButton'); + }); + }); + + describe('no index_patterns privileges', () => { + before(async () => { + await security.role.create('no_index_patterns_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_index_patterns_privileges_user', { + password: 'no_index_patterns_privileges_user-password', + roles: ['no_index_patterns_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_index_patterns_privileges_user', + 'no_index_patterns_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_index_patterns_privileges_role'); + await security.user.delete('no_index_patterns_privileges_user'); + }); + + it('shows Management navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`doesn't show Index Patterns in management side-nav`, async () => { + await PageObjects.settings.navigateTo(); + await testSubjects.existOrFail('kibana'); + await testSubjects.missingOrFail('index_patterns'); + }); + + it(`does not allow navigation to Index Patterns; redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/index_patterns', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.ts new file mode 100644 index 00000000000000..9a3bd5694f76fa --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_spaces.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 expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Management navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Management'); + }); + + it(`index pattern listing shows create button`, async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaIndexPatterns(); + await testSubjects.existOrFail('createIndexPatternButton'); + }); + }); + + describe('space with Index Patterns disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['indexPatterns'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`redirects to Kibana home`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', 'management/kibana/index_patterns', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_patterns/index.ts b/x-pack/test/functional/apps/index_patterns/index.ts new file mode 100644 index 00000000000000..ac5479e9c1c17d --- /dev/null +++ b/x-pack/test/functional/apps/index_patterns/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function advancedSettingsApp({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Index Patterns', function indexPatternsTestSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/index.ts b/x-pack/test/functional/apps/infra/feature_controls/index.ts new file mode 100644 index 00000000000000..269e2b037cf168 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./infrastructure_security')); + loadTestFile(require.resolve('./infrastructure_spaces')); + loadTestFile(require.resolve('./logs_security')); + loadTestFile(require.resolve('./logs_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts new file mode 100644 index 00000000000000..a8a3cec823e709 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_security.ts @@ -0,0 +1,448 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; +import { DATES } from '../constants'; + +const DATE_WITH_DATA = new Date(DATES.metricsAndLogs.hosts.withData); +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('infrastructure security', () => { + describe('global infrastructure all privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_all_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + infrastructure: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_all_user', { + password: 'global_infrastructure_all_user-password', + roles: ['global_infrastructure_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_infrastructure_all_user', + 'global_infrastructure_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_infrastructure_all_role'), + security.user.delete('global_infrastructure_all_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows infrastructure navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Infrastructure', 'Management']); + }); + + describe('infrastructure landing page without data', () => { + it(`shows 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton'); + await testSubjects.existOrFail('configureSourceButton'); + }); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`does not show link to view logs`, async () => { + await testSubjects.missingOrFail('viewLogsContextMenuItem'); + }); + + it(`does not show link to view apm traces`, async () => { + await testSubjects.missingOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + it(`metrics page is visible`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('infraMetricsPage'); + }); + }); + + describe('global infrastructure read privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_read_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_read_user', { + password: 'global_infrastructure_read_user-password', + roles: ['global_infrastructure_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_infrastructure_read_user', + 'global_infrastructure_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_infrastructure_read_role'), + security.user.delete('global_infrastructure_read_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows infrastructure navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Infrastructure', 'Management']); + }); + + describe('infrastructure landing page without data', () => { + it(`doesn't show 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infrastructureViewSetupInstructionsButton'); + await testSubjects.missingOrFail('configureSourceButton'); + }); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`does not show link to view logs`, async () => { + await testSubjects.missingOrFail('viewLogsContextMenuItem'); + }); + + it(`does not show link to view apm traces`, async () => { + await testSubjects.missingOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + it(`metrics page is visible`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('infraMetricsPage'); + }); + }); + + describe('global infrastructure read & logs read privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_logs_read_role', { + elasticsearch: { + indices: [ + { + names: ['metricbeat-*', 'filebeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + logs: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_logs_read_user', { + password: 'global_infrastructure_logs_read_user-password', + roles: ['global_infrastructure_logs_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_infrastructure_logs_read_user', + 'global_infrastructure_logs_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_infrastructure_logs_read_role'); + await security.user.delete('global_infrastructure_logs_read_user'); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`context menu allows user to view logs`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + await testSubjects.click('nodeContainer'); + await testSubjects.click('viewLogsContextMenuItem'); + await testSubjects.existOrFail('infraLogsPage'); + }); + }); + }); + + describe('global infrastructure read & apm privileges', () => { + before(async () => { + await security.role.create('global_infrastructure_apm_read_role', { + elasticsearch: { + indices: [ + { + names: ['metricbeat-*', 'filebeat-*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ + { + feature: { + infrastructure: ['read'], + apm: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_infrastructure_apm_read_user', { + password: 'global_infrastructure_apm_read_user-password', + roles: ['global_infrastructure_apm_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_infrastructure_apm_read_user', + 'global_infrastructure_apm_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_infrastructure_apm_read_role'); + await security.user.delete('global_infrastructure_apm_read_user'); + }); + + describe('infrastructure landing page with data', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + it(`context menu allows user to view APM traces`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + await testSubjects.click('nodeContainer'); + await testSubjects.click('viewApmTracesContextMenuItem'); + await testSubjects.existOrFail('apmMainContainer'); + }); + }); + }); + + describe('global infrastructure no privileges', () => { + before(async () => { + await security.role.create('no_infrastructure_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_infrastructure_privileges_user', { + password: 'no_infrastructure_privileges_user-password', + roles: ['no_infrastructure_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_infrastructure_privileges_user', + 'no_infrastructure_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_infrastructure_privileges_role'); + await security.user.delete('no_infrastructure_privileges_user'); + }); + + it(`doesn't show infrastructure navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.not.contain(['Infrastructure']); + }); + + it(`infrastructure root renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure home page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure landing page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure snapshot page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure/snapshot', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`metrics page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts new file mode 100644 index 00000000000000..8e274e49b15b64 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/infrastructure_spaces.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; +import { DATES } from '../constants'; + +const DATE_WITH_DATA = new Date(DATES.metricsAndLogs.hosts.withData); + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('infrastructure spaces', () => { + before(async () => { + await esArchiver.load('infra/metrics_and_logs'); + }); + + after(async () => { + await esArchiver.unload('infra/metrics_and_logs'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Infrastructure navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Infrastructure'); + }); + + it(`landing page shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`shows link to view logs`, async () => { + await testSubjects.existOrFail('viewLogsContextMenuItem'); + }); + + it(`shows link to view apm traces`, async () => { + await testSubjects.existOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + describe('space with Infrastructure disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['infrastructure'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`doesn't show infrastructure navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Infrastructure'); + }); + + it(`infrastructure root renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure home page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure landing page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`infrastructure snapshot page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'infrastructure/snapshot', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + + it(`metrics page renders not found page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'infraOps', + '/metrics/host/demo-stack-redis-01', + { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + } + ); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + + describe('space with Logs disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['logs'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`landing page shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`doesn't show link to view logs`, async () => { + await testSubjects.missingOrFail('viewLogsContextMenuItem'); + }); + + it(`shows link to view apm traces`, async () => { + await testSubjects.existOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + + describe('space with APM disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['apm'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`landing page shows Wafflemap`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'home', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + }); + await PageObjects.infraHome.goToTime(DATE_WITH_DATA); + await testSubjects.existOrFail('waffleMap'); + }); + + describe('context menu', () => { + before(async () => { + await testSubjects.click('nodeContainer'); + }); + + it(`shows link to view logs`, async () => { + await testSubjects.existOrFail('viewLogsContextMenuItem'); + }); + + it(`doesn't show link to view apm traces`, async () => { + await testSubjects.missingOrFail('viewApmTracesContextMenuItem'); + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts new file mode 100644 index 00000000000000..02f2372a86e197 --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('logs security', () => { + before(async () => { + esArchiver.load('empty_kibana'); + }); + describe('global logs all privileges', () => { + before(async () => { + await security.role.create('global_logs_all_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_all_user', { + password: 'global_logs_all_user-password', + roles: ['global_logs_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login('global_logs_all_user', 'global_logs_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_logs_all_role'), + security.user.delete('global_logs_all_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows logs navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Logs', 'Management']); + }); + + describe('logs landing page without data', () => { + it(`shows 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraLogsPage'); + await testSubjects.existOrFail('logsViewSetupInstructionsButton'); + await testSubjects.existOrFail('configureSourceButton'); + }); + }); + }); + + describe('global logs read privileges', () => { + before(async () => { + await security.role.create('global_logs_read_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + logs: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_read_user', { + password: 'global_logs_read_user-password', + roles: ['global_logs_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_logs_read_user', + 'global_logs_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_logs_read_role'), + security.user.delete('global_logs_read_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it('shows logs navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Logs', 'Management']); + }); + + describe('logs landing page without data', () => { + it(`doesn't show 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraLogsPage'); + await testSubjects.existOrFail('logsViewSetupInstructionsButton'); + await testSubjects.missingOrFail('configureSourceButton'); + }); + }); + }); + + describe('global logs no privileges', () => { + before(async () => { + await security.role.create('global_logs_no_privileges_role', { + elasticsearch: { + indices: [{ names: ['metricbeat-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + infrastructure: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_logs_no_privileges_user', { + password: 'global_logs_no_privileges_user-password', + roles: ['global_logs_no_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.forceLogout(); + + await PageObjects.security.login( + 'global_logs_no_privileges_user', + 'global_logs_no_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_logs_no_privileges_role'), + security.user.delete('global_logs_no_privileges_user'), + PageObjects.security.forceLogout(), + ]); + }); + + it(`doesn't show logs navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.not.contain('Logs'); + }); + + it('logs landing page renders not found page', async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts new file mode 100644 index 00000000000000..5cd94516e1adcc --- /dev/null +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_spaces.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'infraHome', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('logs spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it('shows Logs navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Logs'); + }); + + describe('logs landing page without data', () => { + it(`shows 'Change source configuration' button`, async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraLogsPage'); + await testSubjects.existOrFail('logsViewSetupInstructionsButton'); + await testSubjects.existOrFail('configureSourceButton'); + }); + }); + }); + + describe('space with Logs disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('empty_kibana'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['logs'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('empty_kibana'); + }); + + it(`doesn't show Logs navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.not.contain('Logs'); + }); + + it('logs landing page renders not found page', async () => { + await PageObjects.common.navigateToActualUrl('infraOps', 'logs', { + basePath: '/s/custom_space', + ensureCurrentUrl: true, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('infraNotFoundPage'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/infra/index.ts b/x-pack/test/functional/apps/infra/index.ts index 922e0a48f513f6..d2f293fa834346 100644 --- a/x-pack/test/functional/apps/infra/index.ts +++ b/x-pack/test/functional/apps/infra/index.ts @@ -12,6 +12,7 @@ export default ({ loadTestFile }: KibanaFunctionalTestDefaultProviders) => { this.tags('ciGroup7'); loadTestFile(require.resolve('./home_page')); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./logs_source_configuration')); loadTestFile(require.resolve('./metrics_source_configuration')); }); diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts new file mode 100644 index 00000000000000..5bef6cdb765598 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./ml_security')); + loadTestFile(require.resolve('./ml_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts new file mode 100644 index 00000000000000..a6a7a48e8f3686 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await security.role.delete('global_all_role'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.logout(); + }); + + describe('machine_learning_user', () => { + before(async () => { + await security.user.create('machine_learning_user', { + password: 'machine_learning_user-password', + roles: ['machine_learning_user'], + full_name: 'machine learning user', + }); + }); + + after(async () => { + await security.user.delete('machine_learning_user'); + }); + + it('gets forbidden after login', async () => { + await PageObjects.security.login( + 'machine_learning_user', + 'machine_learning_user-password', + { + expectForbidden: true, + } + ); + }); + }); + + describe('global all', () => { + before(async () => { + await security.user.create('global_all', { + password: 'global_all-password', + roles: ['global_all_role'], + full_name: 'global all', + }); + + await PageObjects.security.login('global_all', 'global_all-password'); + }); + + after(async () => { + await security.user.delete('global_all'); + }); + + it(`doesn't show ml navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Machine Learning'); + }); + }); + + describe('machine_learning_user and global all', () => { + before(async () => { + await security.user.create('machine_learning_user', { + password: 'machine_learning_user-password', + roles: ['machine_learning_user', 'global_all_role'], + full_name: 'machine learning user and global all user', + }); + + await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); + }); + + after(async () => { + await security.user.delete('machine_learning_user'); + }); + + it('shows ML navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Machine Learning'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts new file mode 100644 index 00000000000000..36ae88c759e12e --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Machine Learning navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Machine Learning'); + }); + + it(`can navigate to app`, async () => { + await PageObjects.common.navigateToApp('ml', { + basePath: '/s/custom_space', + }); + + await testSubjects.existOrFail('ml-jobs-list'); + }); + }); + + describe('space with ML disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['ml'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show Machine Learning navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Machine Learning'); + }); + + it(`navigating to app returns a 404`, async () => { + await PageObjects.common.navigateToUrl('ml', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts new file mode 100644 index 00000000000000..14694cdf3fcbe9 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('machine learning', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts new file mode 100644 index 00000000000000..18708b8201bbbe --- /dev/null +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'maps']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText(); + + describe('security feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('maps/data'); + await esArchiver.load('maps/kibana'); + }); + + after(async () => { + await esArchiver.unload('maps/kibana'); + }); + + describe('global maps all privileges', () => { + before(async () => { + await security.role.create('global_maps_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + maps: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_maps_all_user', { + password: 'global_maps_all_user-password', + roles: ['global_maps_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login('global_maps_all_user', 'global_maps_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_maps_all_role'), + security.user.delete('global_maps_all_user'), + PageObjects.security.logout(), + ]); + }); + + it('shows maps navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Maps', 'Management']); + }); + + it(`allows a map to be created`, async () => { + await PageObjects.maps.openNewMap(); + await PageObjects.maps.expectExistAddLayerButton(); + await PageObjects.maps.saveMap('my test map'); + }); + + it(`allows a map to be deleted`, async () => { + await PageObjects.maps.deleteSavedMaps('my test map'); + }); + }); + + describe('global maps read-only privileges', () => { + before(async () => { + await security.role.create('global_maps_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + maps: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_maps_read_user', { + password: 'global_maps_read_user-password', + roles: ['global_maps_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_maps_read_user', + 'global_maps_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_maps_read_role'); + await security.user.delete('global_maps_read_user'); + }); + + it('shows Maps navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Maps', 'Management']); + }); + + it(`does not show create new button`, async () => { + await PageObjects.maps.gotoMapListingPage(); + await PageObjects.maps.expectMissingCreateNewButton(); + }); + + it(`does not allow a map to be deleted`, async () => { + await PageObjects.maps.gotoMapListingPage(); + await testSubjects.missingOrFail('checkboxSelectAll'); + }); + + describe('existing map', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('document example'); + }); + + it(`can't save`, async () => { + await PageObjects.maps.expectMissingSaveButton(); + }); + + it(`can't add layer`, async () => { + await PageObjects.maps.expectMissingAddLayerButton(); + }); + }); + }); + + describe('no maps privileges', () => { + before(async () => { + await security.role.create('no_maps_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_maps_privileges_user', { + password: 'no_maps_privileges_user-password', + roles: ['no_maps_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_maps_privileges_user', + 'no_maps_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_maps_privileges_role'); + await security.user.delete('no_maps_privileges_user'); + }); + + it('does not show Maps navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Discover', 'Management']); + }); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts new file mode 100644 index 00000000000000..d9998beeccd1ec --- /dev/null +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_spaces.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'maps', 'security']); + const appsMenu = getService('appsMenu'); + const find = getService('find'); + + const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText(); + + describe('spaces feature controls', () => { + before(async () => { + await esArchiver.loadIfNeeded('maps/data'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Maps navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Maps'); + }); + + it(`allows a map to be created`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.maps.saveMap('my test map'); + }); + + it(`allows a map to be deleted`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + basePath: `/s/custom_space`, + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.maps.deleteSavedMaps('my test map'); + }); + }); + + describe('space with Maps disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['maps'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('maps', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 49b8611eff8696..9c1aeb10dcc5bb 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -31,6 +31,8 @@ export default function ({ loadTestFile, getService }) { this.tags('ciGroup7'); loadTestFile(require.resolve('./saved_object_management')); loadTestFile(require.resolve('./sample_data')); + loadTestFile(require.resolve('./feature_controls/maps_security')); + loadTestFile(require.resolve('./feature_controls/maps_spaces')); }); describe('', function () { diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/index.ts b/x-pack/test/functional/apps/monitoring/feature_controls/index.ts new file mode 100644 index 00000000000000..35611f391846ec --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./monitoring_security')); + loadTestFile(require.resolve('./monitoring_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts new file mode 100644 index 00000000000000..3e6e9cc67efaee --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('securty', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await security.role.delete('global_all_role'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('monitoring_user', () => { + before(async () => { + await security.user.create('monitoring_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user'], + full_name: 'monitoring all', + }); + }); + + after(async () => { + await security.user.delete('monitoring_user'); + }); + + it('gets forbidden after login', async () => { + await PageObjects.security.login('monitoring_user', 'monitoring_user-password', { + expectForbidden: true, + }); + }); + }); + + describe('global all', () => { + before(async () => { + await security.user.create('global_all', { + password: 'global_all-password', + roles: ['global_all_role'], + full_name: 'global all', + }); + + await PageObjects.security.login('global_all', 'global_all-password'); + }); + + after(async () => { + await security.user.delete('global_all'); + }); + + it(`doesn't show monitoring navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Stack Monitoring'); + }); + }); + + describe('monitoring_user and global all', () => { + before(async () => { + await security.user.create('monitoring_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user', 'global_all_role'], + full_name: 'monitoring user', + }); + + await PageObjects.security.login('monitoring_user', 'monitoring_user-password'); + }); + + after(async () => { + await security.user.delete('monitoring_user'); + }); + + it('shows monitoring navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Stack Monitoring'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts new file mode 100644 index 00000000000000..58282269a5422a --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const appsMenu = getService('appsMenu'); + const find = getService('find'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Stack Monitoring navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Stack Monitoring'); + }); + + it(`can navigate to app`, async () => { + await PageObjects.common.navigateToApp('monitoring', { + basePath: '/s/custom_space', + }); + + const exists = await find.existsByCssSelector('monitoring-main'); + expect(exists).to.be(true); + }); + }); + + describe('space with monitoring disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['monitoring'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show Stack Monitoring navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Stack Monitoring'); + }); + + it(`navigating to app returns a 404`, async () => { + await PageObjects.common.navigateToUrl('monitoring', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index a1551f2829a98a..a22ad50d1338c8 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -8,6 +8,8 @@ export default function ({ loadTestFile }) { describe('Monitoring app', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); loadTestFile(require.resolve('./cluster/alerts')); diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts new file mode 100644 index 00000000000000..956bb3f60871b5 --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -0,0 +1,232 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + + describe('feature controls saved objects management', () => { + before(async () => { + await esArchiver.load('saved_objects_management/feature_controls/security'); + }); + + after(async () => { + await esArchiver.unload('saved_objects_management/feature_controls/security'); + }); + + describe('global all privileges', () => { + before(async () => { + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_all_user', { + password: 'global_all_user-password', + roles: ['global_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login('global_all_user', 'global_all_user-password', { + expectSpaceSelector: false, + }); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_all_role'), + security.user.delete('global_all_user'), + PageObjects.security.logout(), + ]); + }); + + describe('listing', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + + it('shows all saved objects', async () => { + const objects = await PageObjects.settings.getSavedObjectsInTable(); + expect(objects).to.eql(['A Dashboard', 'logstash-*', 'A Pie']); + }); + + it('can view all saved objects in applications', async () => { + const bools = await PageObjects.settings.getSavedObjectsTableSummary(); + expect(bools).to.eql([ + { + title: 'A Dashboard', + canViewInApp: true, + }, + { + title: 'logstash-*', + canViewInApp: true, + }, + { + title: 'A Pie', + canViewInApp: true, + }, + ]); + }); + + it('can delete all saved objects', async () => { + await PageObjects.settings.clickSavedObjectsTableSelectAll(); + const actual = await PageObjects.settings.canSavedObjectsBeDeleted(); + expect(actual).to.be(true); + }); + }); + + describe('edit visualization', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + '/management/kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + { + loginIfPrompted: false, + } + ); + }); + + it('shows delete button', async () => { + await testSubjects.existOrFail('savedObjectEditDelete'); + }); + + it('shows save button', async () => { + await testSubjects.existOrFail('savedObjectEditSave'); + }); + + it('has inputs without readonly attributes', async () => { + const form = await testSubjects.find('savedObjectEditForm'); + const inputs = await form.findAllByCssSelector('input'); + expect(inputs.length).to.be.greaterThan(0); + for (const input of inputs) { + const isEnabled = await input.isEnabled(); + expect(isEnabled).to.be(true); + } + }); + }); + }); + + describe('global visualize read privileges', () => { + before(async () => { + await security.role.create('global_visualize_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_visualize_all_user', { + password: 'global_visualize_all_user-password', + roles: ['global_visualize_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_visualize_all_user', + 'global_visualize_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await Promise.all([ + security.role.delete('global_visualize_all_role'), + security.user.delete('global_visualize_all_user'), + PageObjects.security.logout(), + ]); + }); + + describe('listing', () => { + before(async () => { + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + }); + + it('shows a visualization and an index pattern', async () => { + const objects = await PageObjects.settings.getSavedObjectsInTable(); + expect(objects).to.eql(['logstash-*', 'A Pie']); + }); + + it('can view only the visualization in application', async () => { + const bools = await PageObjects.settings.getSavedObjectsTableSummary(); + expect(bools).to.eql([ + { + title: 'logstash-*', + canViewInApp: false, + }, + { + title: 'A Pie', + canViewInApp: true, + }, + ]); + }); + + it(`can't delete all saved objects`, async () => { + await PageObjects.settings.clickSavedObjectsTableSelectAll(); + const actual = await PageObjects.settings.canSavedObjectsBeDeleted(); + expect(actual).to.be(false); + }); + }); + + describe('edit visualization', () => { + before(async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + '/management/kibana/objects/savedVisualizations/75c3e060-1e7c-11e9-8488-65449e65d0ed', + { + loginIfPrompted: false, + } + ); + await testSubjects.existOrFail('savedObjectsEdit'); + }); + + it('shows delete button', async () => { + await testSubjects.missingOrFail('savedObjectEditDelete'); + }); + + it('shows save button', async () => { + await testSubjects.missingOrFail('savedObjectEditSave'); + }); + + it('has inputs without readonly attributes', async () => { + const form = await testSubjects.find('savedObjectEditForm'); + const inputs = await form.findAllByCssSelector('input'); + expect(inputs.length).to.be.greaterThan(0); + for (const input of inputs) { + const isEnabled = await input.isEnabled(); + expect(isEnabled).to.be(false); + } + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/saved_objects_management/index.ts b/x-pack/test/functional/apps/saved_objects_management/index.ts new file mode 100644 index 00000000000000..b0a0adeb63f1ad --- /dev/null +++ b/x-pack/test/functional/apps/saved_objects_management/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function advancedSettingsApp({ + loadTestFile, +}: KibanaFunctionalTestDefaultProviders) { + describe('Saved objects management', function savedObjectsManagementAppTestSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./feature_controls/saved_objects_management_security')); + }); +} diff --git a/x-pack/test/functional/apps/security/rbac_phase1.js b/x-pack/test/functional/apps/security/rbac_phase1.js index e8c70b0d964452..fbe2f8b66c59b3 100644 --- a/x-pack/test/functional/apps/security/rbac_phase1.js +++ b/x-pack/test/functional/apps/security/rbac_phase1.js @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }) { log.debug('users'); await esArchiver.loadIfNeeded('logstash_functional'); log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); + await esArchiver.load('security/discover'); await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchRoles(); @@ -103,26 +103,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.logout(); }); - it('rbac read only role can not save a visualization', async function () { - const fromTime = '2015-09-19 06:31:44.000'; - const toTime = '2015-09-23 18:31:44.000'; - const vizName1 = 'Viz VerticalBarChart'; - - log.debug('log in as kibanareadonly with rbac_read role'); - await PageObjects.security.login('kibanareadonly', 'changeme'); - log.debug('navigateToApp visualize'); - await PageObjects.visualize.navigateToNewVisualization(); - log.debug('clickVerticalBarChart'); - await PageObjects.visualize.clickVerticalBarChart(); - await PageObjects.visualize.clickNewSearch(); - log.debug('Set absolute time range from \"' + fromTime + '\" to \"' + toTime + '\"'); - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.visualize.waitForVisualization(); - await PageObjects.visualize.saveVisualizationExpectFail(vizName1); - await PageObjects.security.logout(); - - }); - after(async function () { await PageObjects.security.logout(); }); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index fb5effa6277566..4f725d04ce034c 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -24,7 +24,7 @@ export default function ({ getService, getPageObjects }) { log.debug('users'); await esArchiver.loadIfNeeded('logstash_functional'); log.debug('load kibana index with default index pattern'); - await esArchiver.load('discover'); + await esArchiver.load('security/discover'); await kibanaServer.uiSettings.replace({ 'defaultIndex': 'logstash-*' }); await PageObjects.settings.navigateTo(); }); @@ -64,15 +64,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.login('Rashmi', 'changeme'); }); - //Verify the Access Denied message is displayed - it('Kibana User navigating to Monitoring gets Access Denied', async function () { - const expectedMessage = 'Access Denied'; - await PageObjects.monitoring.navigateTo(); - const actualMessage = await PageObjects.monitoring.getAccessDeniedMessage(); - expect(actualMessage).to.be(expectedMessage); - }); - - it('Kibana User navigating to Management gets permission denied', async function () { await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); diff --git a/x-pack/test/functional/apps/security/user_email.js b/x-pack/test/functional/apps/security/user_email.js index d4828b61053d6d..b79e32fc6a7dea 100644 --- a/x-pack/test/functional/apps/security/user_email.js +++ b/x-pack/test/functional/apps/security/user_email.js @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }) { describe('useremail', function () { before(async () => { - await esArchiver.load('discover'); + await esArchiver.load('security/discover'); await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); }); diff --git a/x-pack/test/functional/apps/spaces/index.ts b/x-pack/test/functional/apps/spaces/index.ts index 3b8a1ad3405b7d..b8b4e71b141fea 100644 --- a/x-pack/test/functional/apps/spaces/index.ts +++ b/x-pack/test/functional/apps/spaces/index.ts @@ -3,10 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { TestInvoker } from './lib/types'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export -export default function spacesApp({ loadTestFile }: TestInvoker) { +export default function spacesApp({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { describe('Spaces app', function spacesAppTestSuite() { this.tags('ciGroup4'); diff --git a/x-pack/test/functional/apps/spaces/lib/types.ts b/x-pack/test/functional/apps/spaces/lib/types.ts deleted file mode 100644 index 2ed91406e5f48d..00000000000000 --- a/x-pack/test/functional/apps/spaces/lib/types.ts +++ /dev/null @@ -1,27 +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. - */ - -export type DescribeFn = (text: string, fn: () => void) => void; - -export interface TestDefinitionAuthentication { - username?: string; - password?: string; -} - -export type LoadTestFileFn = (path: string) => string; - -export type GetServiceFn = (service: string) => any; - -export type ReadConfigFileFn = (path: string) => any; - -export type GetPageObjectsFn = (pageObjects: string[]) => any; - -export interface TestInvoker { - getService: GetServiceFn; - getPageObjects: GetPageObjectsFn; - loadTestFile: LoadTestFileFn; - readConfigFile: ReadConfigFileFn; -} diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index 92ab677311702f..578ca05dbee9c1 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { TestInvoker } from './lib/types'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; // eslint-disable-next-line import/no-default-export -export default function spaceSelectorFunctonalTests({ getService, getPageObjects }: TestInvoker) { - const config = getService('config'); +export default function spaceSelectorFunctonalTests({ + getService, + getPageObjects, +}: KibanaFunctionalTestDefaultProviders) { const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects([ 'common', @@ -20,8 +22,8 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects describe('Spaces', () => { describe('Space Selector', () => { - before(async () => await esArchiver.load('spaces')); - after(async () => await esArchiver.unload('spaces')); + before(async () => await esArchiver.load('spaces/selector')); + after(async () => await esArchiver.unload('spaces/selector')); afterEach(async () => { await PageObjects.security.logout(); @@ -50,8 +52,6 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects describe('Spaces Data', () => { const spaceId = 'another-space'; - const dashboardPath = config.get(['apps', 'dashboard']).pathname; - const homePath = config.get(['apps', 'home']).pathname; const sampleDataHash = '/home/tutorial_directory/sampleData'; const expectDashboardRenders = async (dashName: string) => { @@ -62,22 +62,18 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects }; before(async () => { - await esArchiver.load('spaces'); + await esArchiver.load('spaces/selector'); await PageObjects.security.login(null, null, { expectSpaceSelector: true, }); await PageObjects.spaceSelector.clickSpaceCard('default'); await PageObjects.common.navigateToApp('home', { - appConfig: { - hash: sampleDataHash, - }, + hash: sampleDataHash, }); await PageObjects.home.addSampleDataSet('logs'); await PageObjects.common.navigateToApp('home', { - appConfig: { - hash: sampleDataHash, - pathname: `/s/${spaceId}${homePath}`, - }, + hash: sampleDataHash, + basePath: `/s/${spaceId}`, }); await PageObjects.home.addSampleDataSet('logs'); }); @@ -88,13 +84,11 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects // the created saved objects in the second space will be broken but removed // when we call esArchiver.unload('spaces'). await PageObjects.common.navigateToApp('home', { - appConfig: { - hash: sampleDataHash, - }, + hash: sampleDataHash, }); await PageObjects.home.removeSampleDataSet('logs'); await PageObjects.security.logout(); - await esArchiver.unload('spaces'); + await esArchiver.unload('spaces/selector'); }); describe('displays separate data for each space', async () => { @@ -105,9 +99,7 @@ export default function spaceSelectorFunctonalTests({ getService, getPageObjects it('in a custom space', async () => { await PageObjects.common.navigateToApp('dashboard', { - appConfig: { - pathname: `/s/${spaceId}${dashboardPath}`, - }, + basePath: `/s/${spaceId}`, }); await expectDashboardRenders('[Logs] Web Traffic'); }); diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index e3f3451574771d..88203b350c262c 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -15,6 +15,7 @@ export default function statusPageFunctonalTests({ getService, getPageObjects }: after(async () => await esArchiver.unload('empty_kibana')); it('allows user to navigate without authentication', async () => { + await PageObjects.security.logout(); await PageObjects.statusPage.navigateToPage(); await PageObjects.statusPage.expectStatusPage(); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts new file mode 100644 index 00000000000000..77cfa8e48a4183 --- /dev/null +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'timelion', 'header', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + describe('feature controls security', () => { + before(async () => { + await esArchiver.loadIfNeeded('timelion/feature_controls'); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('global timelion all privileges', () => { + before(async () => { + await security.role.create('global_timelion_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + timelion: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_timelion_all_user', { + password: 'global_timelion_all_user-password', + roles: ['global_timelion_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_timelion_all_user', + 'global_timelion_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_timelion_all_role'); + await security.user.delete('global_timelion_all_user'); + }); + + it('shows timelion navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Timelion', 'Management']); + }); + + it(`allows a timelion sheet to be created`, async () => { + await PageObjects.common.navigateToApp('timelion'); + await PageObjects.timelion.saveTimelionSheet(); + }); + }); + + describe('global timelion read-only privileges', () => { + before(async () => { + await security.role.create('global_timelion_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + timelion: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_timelion_read_user', { + password: 'global_timelion_read_user-password', + roles: ['global_timelion_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_timelion_read_user', + 'global_timelion_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_timelion_read_role'); + await security.user.delete('global_timelion_read_user'); + }); + + it('shows timelion navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Timelion', 'Management']); + }); + + it(`does not allow a timelion sheet to be created`, async () => { + await PageObjects.common.navigateToApp('timelion'); + await PageObjects.timelion.expectMissingWriteControls(); + }); + }); + + describe('no timelion privileges', () => { + before(async () => { + await security.role.create('no_timelion_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_timelion_privileges_user', { + password: 'no_timelion_privileges_user-password', + roles: ['no_timelion_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'no_timelion_privileges_user', + 'no_timelion_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('no_timelion_privileges_role'); + await security.user.delete('no_timelion_privileges_user'); + }); + + const getMessageText = async () => + await (await find.byCssSelector('body>pre')).getVisibleText(); + + it(`returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts new file mode 100644 index 00000000000000..68a3000a0c2350 --- /dev/null +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_spaces.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'timelion', 'security', 'spaceSelector']); + const find = getService('find'); + const appsMenu = getService('appsMenu'); + + const getMessageText = async () => await (await find.byCssSelector('body>pre')).getVisibleText(); + + describe('timelion', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('timelion/feature_controls'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('timelion/feature_controls'); + }); + + it('shows timelion navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Timelion'); + }); + + it(`allows a timelion sheet to be created`, async () => { + await PageObjects.common.navigateToApp('timelion'); + await PageObjects.timelion.saveTimelionSheet(); + }); + }); + + describe('space with Timelion disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('timelion/feature_controls'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['timelion'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('timelion/feature_controls'); + }); + + it(`doesn't show timelion navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Timelion'); + }); + + it(`create new timelion returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', 'i-exist', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`edit timelion sheet which doesn't exist returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', 'i-dont-exist', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + + it(`edit timelion sheet which exists returns a 404`, async () => { + await PageObjects.common.navigateToActualUrl('timelion', 'i-exist', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + + const messageText = await getMessageText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }) + ); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/timelion/index.ts b/x-pack/test/functional/apps/timelion/index.ts new file mode 100644 index 00000000000000..2dfed6f89d9e34 --- /dev/null +++ b/x-pack/test/functional/apps/timelion/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function timelion({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Timelion', function visualizeTestSuite() { + this.tags('ciGroup4'); + + loadTestFile(require.resolve('./feature_controls/timelion_security')); + loadTestFile(require.resolve('./feature_controls/timelion_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/uptime/feature_controls/index.ts b/x-pack/test/functional/apps/uptime/feature_controls/index.ts new file mode 100644 index 00000000000000..d4a948ea97e5cb --- /dev/null +++ b/x-pack/test/functional/apps/uptime/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./uptime_security')); + loadTestFile(require.resolve('./uptime_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts new file mode 100644 index 00000000000000..ae563b43063648 --- /dev/null +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_security.ts @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('global uptime all privileges', () => { + before(async () => { + await security.role.create('global_uptime_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_uptime_all_user', { + password: 'global_uptime_all_user-password', + roles: ['global_uptime_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_uptime_all_user', + 'global_uptime_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_uptime_all_role'); + await security.user.delete('global_uptime_all_user'); + }); + + it('shows uptime navlink', async () => { + const navLinks = await appsMenu.readLinks(); + expect(navLinks.map((link: Record) => link.text)).to.eql([ + 'Uptime', + 'Management', + ]); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('uptime'); + await testSubjects.existOrFail('uptimeApp', 10000); + }); + }); + + describe('global uptime read-only privileges', () => { + before(async () => { + await security.role.create('global_uptime_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + uptime: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_uptime_read_user', { + password: 'global_uptime_read_user-password', + roles: ['global_uptime_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_uptime_read_user', + 'global_uptime_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('global_uptime_read_role'); + await security.user.delete('global_uptime_read_user'); + }); + + it('shows uptime navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Uptime', 'Management']); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('uptime'); + await testSubjects.existOrFail('uptimeApp', 10000); + }); + }); + + describe('no uptime privileges', () => { + before(async () => { + await security.role.create('no_uptime_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_uptime_privileges_user', { + password: 'no_uptime_privileges_user-password', + roles: ['no_uptime_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_uptime_privileges_user', + 'no_uptime_privileges_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await security.role.delete('no_uptime_privileges_role'); + await security.user.delete('no_uptime_privileges_user'); + }); + + it(`doesn't show uptime navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Uptime'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('uptime', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts new file mode 100644 index 00000000000000..cf23bff4be775a --- /dev/null +++ b/x-pack/test/functional/apps/uptime/feature_controls/uptime_spaces.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'error', 'timePicker', 'security']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('spaces', () => { + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows uptime navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Uptime'); + }); + + it('can navigate to Uptime app', async () => { + await PageObjects.common.navigateToApp('uptime'); + await testSubjects.existOrFail('uptimeApp', 10000); + }); + }); + + describe('space with Uptime disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['uptime'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show uptime navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Uptime'); + }); + + it(`renders not found page`, async () => { + await PageObjects.common.navigateToUrl('uptime', '', { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/uptime/index.ts b/x-pack/test/functional/apps/uptime/index.ts index fdc97a1128aadf..7a53ce7d32aa15 100644 --- a/x-pack/test/functional/apps/uptime/index.ts +++ b/x-pack/test/functional/apps/uptime/index.ts @@ -21,6 +21,7 @@ export default ({ loadTestFile, getService }: KibanaFunctionalTestDefaultProvide after(async () => await esArchiver.unload(ARCHIVE)); this.tags('ciGroup6'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./overview')); loadTestFile(require.resolve('./monitor')); }); diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts new file mode 100644 index 00000000000000..fdf22ff7bede99 --- /dev/null +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -0,0 +1,247 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects([ + 'common', + 'visualize', + 'header', + 'security', + 'share', + 'spaceSelector', + 'timePicker', + ]); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('feature controls security', () => { + before(async () => { + await esArchiver.load('visualize/default'); + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + after(async () => { + await esArchiver.unload('visualize/default'); + }); + + describe('global visualize all privileges', () => { + before(async () => { + await security.role.create('global_visualize_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + visualize: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_visualize_all_user', { + password: 'global_visualize_all_user-password', + roles: ['global_visualize_all_role'], + full_name: 'test user', + }); + + await PageObjects.security.logout(); + + await PageObjects.security.login( + 'global_visualize_all_user', + 'global_visualize_all_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_visualize_all_role'); + await security.user.delete('global_visualize_all_user'); + }); + + it('shows visualize navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Visualize', 'Management']); + }); + + it(`landing page shows "Create new Visualization" button`, async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await testSubjects.existOrFail('visualizeLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`can view existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('visualizationLoader', 10000); + }); + + it('can save existing Visualization', async () => { + await PageObjects.common.navigateToActualUrl('kibana', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('visualizeSaveButton', 10000); + }); + + it('Embed code shows create short-url button', async () => { + await PageObjects.share.openShareMenuItem('Embedcode'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + + it('Permalinks shows create short-url button', async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlExistOrFail(); + }); + }); + + describe('global visualize read-only privileges', () => { + before(async () => { + await security.role.create('global_visualize_read_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + visualize: ['read'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('global_visualize_read_user', { + password: 'global_visualize_read_user-password', + roles: ['global_visualize_read_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'global_visualize_read_user', + 'global_visualize_read_user-password', + { + expectSpaceSelector: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('global_visualize_read_role'); + await security.user.delete('global_visualize_read_user'); + }); + + it('shows visualize navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.eql(['Visualize', 'Management']); + }); + + it(`landing page shows "Create new Visualization" button`, async () => { + await PageObjects.visualize.gotoVisualizationLandingPage(); + await testSubjects.existOrFail('visualizeLandingPage', 10000); + await testSubjects.existOrFail('newItemButton'); + }); + + it(`can view existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('visualizationLoader', 10000); + }); + + it(`can't save existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('shareTopNavButton', 10000); + await testSubjects.missingOrFail('visualizeSaveButton', 10000); + }); + + it(`Embed Code doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Embedcode'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + + it(`Permalinks doesn't show create short-url button`, async () => { + await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.createShortUrlMissingOrFail(); + }); + }); + + describe('no visualize privileges', () => { + before(async () => { + await security.role.create('no_visualize_privileges_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + feature: { + discover: ['all'], + }, + spaces: ['*'], + }, + ], + }); + + await security.user.create('no_visualize_privileges_user', { + password: 'no_visualize_privileges_user-password', + roles: ['no_visualize_privileges_role'], + full_name: 'test user', + }); + + await PageObjects.security.login( + 'no_visualize_privileges_user', + 'no_visualize_privileges_user-password', + { + expectSpaceSelector: false, + shouldLoginIfPrompted: false, + } + ); + }); + + after(async () => { + await PageObjects.security.logout(); + await security.role.delete('no_visualize_privileges_role'); + await security.user.delete('no_visualize_privileges_user'); + }); + + it(`landing page redirects to home page`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit page redirects to home page`, async () => { + await PageObjects.common.navigateToActualUrl('visualize', '/visualize/edit/i-exist', { + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts new file mode 100644 index 00000000000000..e69878f3200dd2 --- /dev/null +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_spaces.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +// eslint-disable-next-line max-len +import { VisualizeConstants } from '../../../../../../src/legacy/core_plugins/kibana/public/visualize/visualize_constants'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'visualize', 'security', 'spaceSelector']); + const testSubjects = getService('testSubjects'); + const appsMenu = getService('appsMenu'); + + describe('visualize', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + }); + + describe('space with no features disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('visualize/default'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('visualize/default'); + }); + + it('shows visualize navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Visualize'); + }); + + it(`can view existing Visualization`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + `${VisualizeConstants.EDIT_PATH}/i-exist`, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('visualizationLoader', 10000); + }); + }); + + describe('space with Visualize disabled', () => { + before(async () => { + // we need to load the following in every situation as deleting + // a space deletes all of the associated saved objects + await esArchiver.load('visualize/default'); + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['visualize'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + await esArchiver.unload('visualize/default'); + }); + + it(`doesn't show visualize navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Visualize'); + }); + + it(`create new visualization redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl('kibana', VisualizeConstants.CREATE_PATH, { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + }); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit visualization for object which doesn't exist redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + `${VisualizeConstants.EDIT_PATH}/i-dont-exist`, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + + it(`edit visualization for object which exists redirects to the home page`, async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + `${VisualizeConstants.EDIT_PATH}/i-exist`, + { + basePath: '/s/custom_space', + ensureCurrentUrl: false, + shouldLoginIfPrompted: false, + } + ); + await testSubjects.existOrFail('homeApp', 10000); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/visualize/index.ts b/x-pack/test/functional/apps/visualize/index.ts new file mode 100644 index 00000000000000..0469a4f0d2cd6d --- /dev/null +++ b/x-pack/test/functional/apps/visualize/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function visualize({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('Visualize', function visualizeTestSuite() { + this.tags('ciGroup4'); + + loadTestFile(require.resolve('./feature_controls/visualize_security')); + loadTestFile(require.resolve('./feature_controls/visualize_spaces')); + }); +} diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 0596de6e622d58..5c542ea3e17a87 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -9,6 +9,7 @@ import { resolve } from 'path'; import { + CanvasPageProvider, SecurityPageProvider, MonitoringPageProvider, LogstashPageProvider, @@ -59,6 +60,11 @@ import { InfraSourceConfigurationFlyoutProvider, } from './services'; +import { + SecurityServiceProvider, + SpacesServiceProvider, +} from '../common/services'; + // the default export of config files must be a config provider // that returns an object with the projects config values export default async function ({ readConfigFile }) { @@ -75,21 +81,31 @@ export default async function ({ readConfigFile }) { return { // list paths to the files that contain your plugins tests testFiles: [ + resolve(__dirname, './apps/advanced_settings'), resolve(__dirname, './apps/canvas'), resolve(__dirname, './apps/graph'), resolve(__dirname, './apps/monitoring'), resolve(__dirname, './apps/watcher'), + resolve(__dirname, './apps/dashboard'), resolve(__dirname, './apps/dashboard_mode'), + resolve(__dirname, './apps/discover'), resolve(__dirname, './apps/security'), resolve(__dirname, './apps/spaces'), resolve(__dirname, './apps/logstash'), resolve(__dirname, './apps/grok_debugger'), resolve(__dirname, './apps/infra'), + resolve(__dirname, './apps/machine_learning'), resolve(__dirname, './apps/rollup_job'), resolve(__dirname, './apps/maps'), resolve(__dirname, './apps/status_page'), + resolve(__dirname, './apps/timelion'), resolve(__dirname, './apps/upgrade_assistant'), + resolve(__dirname, './apps/visualize'), resolve(__dirname, './apps/uptime'), + resolve(__dirname, './apps/saved_objects_management'), + resolve(__dirname, './apps/dev_tools'), + resolve(__dirname, './apps/apm'), + resolve(__dirname, './apps/index_patterns') ], // define the name and providers for services that should be @@ -124,6 +140,8 @@ export default async function ({ readConfigFile }) { random: RandomProvider, aceEditor: AceEditorProvider, grokDebugger: GrokDebuggerProvider, + security: SecurityServiceProvider, + spaces: SpacesServiceProvider, userMenu: UserMenuProvider, uptime: UptimeProvider, rollup: RollupPageProvider, @@ -134,6 +152,7 @@ export default async function ({ readConfigFile }) { // names to Providers. Merge in Kibana's or pick specific ones pageObjects: { ...kibanaFunctionalConfig.get('pageObjects'), + canvas: CanvasPageProvider, security: SecurityPageProvider, accountSetting: AccountSettingProvider, monitoring: MonitoringPageProvider, @@ -202,6 +221,10 @@ export default async function ({ readConfigFile }) { pathname: '/app/kibana', hash: '/dev_tools/grokdebugger', }, + searchProfiler: { + pathname: '/app/kibana', + hash: '/dev_tools/searchprofiler', + }, spaceSelector: { pathname: '/', }, @@ -219,10 +242,19 @@ export default async function ({ readConfigFile }) { uptime: { pathname: '/app/uptime', }, + apm: { + pathname: '/app/apm' + }, + ml: { + pathname: '/app/ml' + }, rollupJob: { pathname: '/app/kibana', hash: '/management/elasticsearch/rollup_jobs/', }, + apm: { + pathname: '/app/apm', + } }, // choose where esArchiver should load archives from diff --git a/x-pack/test/functional/es_archives/canvas/default/mappings.json b/x-pack/test/functional/es_archives/canvas/default/mappings.json index ba7691797e320b..3bde3969e5ded1 100644 --- a/x-pack/test/functional/es_archives/canvas/default/mappings.json +++ b/x-pack/test/functional/es_archives/canvas/default/mappings.json @@ -224,6 +224,9 @@ "initials": { "type": "keyword" }, + "disabledFeatures": { + "type": "keyword" + }, "name": { "fields": { "keyword": { diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json new file mode 100644 index 00000000000000..f085bad4c507ec --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/data.json @@ -0,0 +1,85 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:i-exist", + "source": { + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json new file mode 100644 index 00000000000000..2956597f5cf16a --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/security/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json new file mode 100644 index 00000000000000..a43c72659b4072 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/data.json @@ -0,0 +1,127 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "namespace": "custom_space", + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:dashboard:i-exist", + "source": { + "namespace": "custom_space", + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:config:6.0.0", + "source": { + "namespace": "custom_space", + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json new file mode 100644 index 00000000000000..35696c187537f3 --- /dev/null +++ b/x-pack/test/functional/es_archives/dashboard/feature_controls/spaces/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json b/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json index adf4050bb88c43..3558e89558a56a 100644 --- a/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json +++ b/x-pack/test/functional/es_archives/dashboard_view_mode/mappings.json @@ -175,6 +175,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -299,4 +302,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json new file mode 100644 index 00000000000000..3c0613005c9508 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/security/data.json @@ -0,0 +1,37 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json new file mode 100644 index 00000000000000..35696c187537f3 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/security/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json new file mode 100644 index 00000000000000..af5aa6d0434847 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/data.json @@ -0,0 +1,77 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:config:6.0.0", + "source": { + "namespace": "custom_space", + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json new file mode 100644 index 00000000000000..35696c187537f3 --- /dev/null +++ b/x-pack/test/functional/es_archives/discover/feature_controls/spaces/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/empty_kibana/mappings.json b/x-pack/test/functional/es_archives/empty_kibana/mappings.json index 35e3a46a07bba2..77eac534850a55 100644 --- a/x-pack/test/functional/es_archives/empty_kibana/mappings.json +++ b/x-pack/test/functional/es_archives/empty_kibana/mappings.json @@ -154,6 +154,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -278,4 +281,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/logstash/empty/mappings.json b/x-pack/test/functional/es_archives/logstash/empty/mappings.json index 9e56ff8a7a43a5..096c68aefcc3cb 100644 --- a/x-pack/test/functional/es_archives/logstash/empty/mappings.json +++ b/x-pack/test/functional/es_archives/logstash/empty/mappings.json @@ -186,6 +186,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -310,4 +313,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/maps/kibana/mappings.json b/x-pack/test/functional/es_archives/maps/kibana/mappings.json index e60e5440d9a2ac..c7e786b20ac19a 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/mappings.json +++ b/x-pack/test/functional/es_archives/maps/kibana/mappings.json @@ -279,6 +279,9 @@ "initials": { "type": "keyword" }, + "disabledFeatures": { + "type": "keyword" + }, "name": { "fields": { "keyword": { diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json new file mode 100644 index 00000000000000..f085bad4c507ec --- /dev/null +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/data.json @@ -0,0 +1,85 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "dashboard:i-exist", + "source": { + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:6.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json new file mode 100644 index 00000000000000..2956597f5cf16a --- /dev/null +++ b/x-pack/test/functional/es_archives/saved_objects_management/feature_controls/security/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/discover/data.json.gz b/x-pack/test/functional/es_archives/security/discover/data.json.gz similarity index 100% rename from x-pack/test/functional/es_archives/discover/data.json.gz rename to x-pack/test/functional/es_archives/security/discover/data.json.gz diff --git a/x-pack/test/functional/es_archives/discover/mappings.json b/x-pack/test/functional/es_archives/security/discover/mappings.json similarity index 98% rename from x-pack/test/functional/es_archives/discover/mappings.json rename to x-pack/test/functional/es_archives/security/discover/mappings.json index adf4050bb88c43..3558e89558a56a 100644 --- a/x-pack/test/functional/es_archives/discover/mappings.json +++ b/x-pack/test/functional/es_archives/security/discover/mappings.json @@ -175,6 +175,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -299,4 +302,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/spaces/disabled_features/data.json b/x-pack/test/functional/es_archives/spaces/disabled_features/data.json new file mode 100644 index 00000000000000..0826c7dcf164bf --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/disabled_features/data.json @@ -0,0 +1,113 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:default", + "source": { + "space": { + "name": "Default", + "description": "This is the default space!", + "disabledFeatures": [], + "_reserved": true + }, + "type": "space", + "migrationVersion": { + "space": "6.6.0" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:visualization:75c3e060-1e7c-11e9-8488-65449e65d0ed", + "source": { + "namespace": "custom_space", + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:dashboard:i-exist", + "source": { + "namespace": "custom_space", + "dashboard": { + "title": "A Dashboard", + "hits": 0, + "description": "", + "panelsJSON": "[{\"gridData\":{\"w\":24,\"h\":15,\"x\":0,\"y\":0,\"i\":\"1\"},\"version\":\"7.0.0\",\"panelIndex\":\"1\",\"type\":\"visualization\",\"id\":\"75c3e060-1e7c-11e9-8488-65449e65d0ed\",\"embeddableConfig\":{}}]", + "optionsJSON": "{\"darkTheme\":false,\"useMargins\":true,\"hidePanelTitles\":false}", + "version": 1, + "timeRestore": false, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "dashboard", + "updated_at": "2019-01-22T19:32:47.232Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json b/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json new file mode 100644 index 00000000000000..2956597f5cf16a --- /dev/null +++ b/x-pack/test/functional/es_archives/spaces/disabled_features/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/spaces/data.json b/x-pack/test/functional/es_archives/spaces/selector/data.json similarity index 100% rename from x-pack/test/functional/es_archives/spaces/data.json rename to x-pack/test/functional/es_archives/spaces/selector/data.json diff --git a/x-pack/test/functional/es_archives/spaces/mappings.json b/x-pack/test/functional/es_archives/spaces/selector/mappings.json similarity index 98% rename from x-pack/test/functional/es_archives/spaces/mappings.json rename to x-pack/test/functional/es_archives/spaces/selector/mappings.json index 35e3a46a07bba2..77eac534850a55 100644 --- a/x-pack/test/functional/es_archives/spaces/mappings.json +++ b/x-pack/test/functional/es_archives/spaces/selector/mappings.json @@ -154,6 +154,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -278,4 +281,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/es_archives/timelion/feature_controls/data.json b/x-pack/test/functional/es_archives/timelion/feature_controls/data.json new file mode 100644 index 00000000000000..03fc0d57e92780 --- /dev/null +++ b/x-pack/test/functional/es_archives/timelion/feature_controls/data.json @@ -0,0 +1,81 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "config:7.0.0", + "source": { + "config": { + "buildNum": 9007199254740991, + "defaultIndex": "logstash-*" + }, + "type": "config", + "updated_at": "2019-01-22T19:32:02.235Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "index-pattern:logstash-*", + "source": { + "type": "index-pattern", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"kilobytes\",\"type\":\"number\",\"count\":0,\"scripted\":true,\"script\":\"doc['bytes'].value / 1000\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"machine os raw\",\"type\":\"string\",\"count\":0,\"scripted\":true,\"script\":\"doc['machine.os.raw'].value\",\"lang\":\"painless\",\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false}]" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "timelion-sheet:i-exist", + "source": { + "timelion-sheet": { + "title": "i-exist", + "hits": 0, + "description": "", + "timelion_sheet": [ + ".es(*)" + ], + "timelion_interval": "auto", + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_rows": 2, + "version": 1 + }, + "type": "timelion-sheet" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "id": "custom_space:timelion-sheet:i-exist", + "source": { + "namespace": "custom_space", + "timelion-sheet": { + "title": "i-exist", + "hits": 0, + "description": "", + "timelion_sheet": [ + ".es(*).label('custom space sheet')" + ], + "timelion_interval": "auto", + "timelion_chart_height": 275, + "timelion_columns": 2, + "timelion_rows": 2, + "version": 1 + }, + "type": "timelion-sheet" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json b/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json new file mode 100644 index 00000000000000..2956597f5cf16a --- /dev/null +++ b/x-pack/test/functional/es_archives/timelion/feature_controls/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/es_archives/visualize/default/data.json b/x-pack/test/functional/es_archives/visualize/default/data.json new file mode 100644 index 00000000000000..250af4d5c5c132 --- /dev/null +++ b/x-pack/test/functional/es_archives/visualize/default/data.json @@ -0,0 +1,110 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "space:default", + "source": { + "space": { + "name": "Default", + "description": "This is the default space!", + "disabledFeatures": [], + "_reserved": true + }, + "type": "space", + "migrationVersion": { + "space": "6.6.0" + } + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "index-pattern:logstash-*", + "source": { + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:index-pattern:logstash-*", + "source": { + "namespace": "custom_space", + "index-pattern": { + "title": "logstash-*", + "timeFieldName": "@timestamp", + "fields": "[{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"searchable\":false,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":false},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":false,\"readFromDocValues\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"searchable\":true,\"aggregatable\":true,\"readFromDocValues\":true}]" + }, + "type": "index-pattern", + "migrationVersion": { + "index-pattern": "6.5.0" + }, + "updated_at": "2018-12-21T00:43:07.096Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "visualization:i-exist", + "source": { + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "custom_space:visualization:i-exist", + "source": { + "namespace": "custom_space", + "visualization": { + "title": "A Pie", + "visState": "{\"title\":\"A Pie\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"isDonut\":true,\"labels\":{\"show\":false,\"values\":true,\"last_level\":true,\"truncate\":100},\"dimensions\":{\"metric\":{\"accessor\":0,\"format\":{\"id\":\"number\"},\"params\":{},\"aggType\":\"count\"}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{}},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"schema\":\"segment\",\"params\":{\"field\":\"geo.src\",\"size\":5,\"order\":\"desc\",\"orderBy\":\"1\",\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"}}]}", + "uiStateJSON": "{}", + "description": "", + "version": 1, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"index\":\"logstash-*\",\"query\":{\"query\":\"\",\"language\":\"lucene\"},\"filter\":[]}" + } + }, + "type": "visualization", + "updated_at": "2019-01-22T19:32:31.206Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/visualize/default/mappings.json b/x-pack/test/functional/es_archives/visualize/default/mappings.json new file mode 100644 index 00000000000000..35696c187537f3 --- /dev/null +++ b/x-pack/test/functional/es_archives/visualize/default/mappings.json @@ -0,0 +1,461 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "doc": { + "dynamic": "strict", + "properties": { + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "gis-map" : { + "properties" : { + "bounds" : { + "type" : "geo_shape", + "tree" : "quadtree" + }, + "description" : { + "type" : "text" + }, + "layerListJSON" : { + "type" : "text" + }, + "mapStateJSON" : { + "type" : "text" + }, + "title" : { + "type" : "text" + }, + "uiStateJSON" : { + "type" : "text" + }, + "version" : { + "type" : "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + } + } + } +} diff --git a/x-pack/test/functional/page_objects/canvas_page.ts b/x-pack/test/functional/page_objects/canvas_page.ts new file mode 100644 index 00000000000000..69eb3cc36e8f8b --- /dev/null +++ b/x-pack/test/functional/page_objects/canvas_page.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { KibanaFunctionalTestDefaultProviders } from '../../types/providers'; + +export function CanvasPageProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const testSubjects = getService('testSubjects'); + const find = getService('find'); + + return { + async expectCreateWorkpadButtonEnabled() { + const button = await testSubjects.find('create-workpad-button'); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be(null); + }, + + async expectCreateWorkpadButtonDisabled() { + const button = await testSubjects.find('create-workpad-button'); + const disabledAttr = await button.getAttribute('disabled'); + expect(disabledAttr).to.be('true'); + }, + + async expectAddElementButton() { + await testSubjects.existOrFail('add-element-button'); + }, + + async expectNoAddElementButton() { + // Ensure page is fully loaded first by waiting for the refresh button + const refreshPopoverExists = await find.existsByCssSelector('#auto-refresh-popover', 20000); + expect(refreshPopoverExists).to.be(true); + + const addElementButtonExists = await find.existsByCssSelector( + 'button[data-test-subj=add-element-button]', + 10 // don't need much of a wait at all here, because we already waited for refresh button above + ); + expect(addElementButtonExists).to.be(false); + }, + }; +} diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index 6d5d763d73a4f1..f7714b2a62f941 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -129,6 +129,22 @@ export function GisPageProvider({ getService, getPageObjects }) { await testSubjects.clickWhenNotDisabled('confirmSaveSavedObjectButton'); } + async expectMissingSaveButton() { + await testSubjects.missingOrFail('mapSaveButton'); + } + + async expectMissingCreateNewButton() { + await testSubjects.missingOrFail('newMapLink'); + } + + async expectMissingAddLayerButton() { + await testSubjects.missingOrFail('addLayerButton'); + } + + async expectExistAddLayerButton() { + await testSubjects.existOrFail('addLayerButton'); + } + async onMapListingPage() { log.debug(`onMapListingPage`); const exists = await testSubjects.exists('mapsListingPage'); diff --git a/x-pack/test/functional/page_objects/index.js b/x-pack/test/functional/page_objects/index.js index 0eb5c77fe11f8e..571d9c4aef5ebc 100644 --- a/x-pack/test/functional/page_objects/index.js +++ b/x-pack/test/functional/page_objects/index.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export { CanvasPageProvider } from './canvas_page'; export { SecurityPageProvider } from './security_page'; export { MonitoringPageProvider } from './monitoring_page'; export { LogstashPageProvider } from './logstash_page'; diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 1f9ca10ef30333..fde1eedbfdf531 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -16,7 +16,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const userMenu = getService('userMenu'); - const PageObjects = getPageObjects(['common', 'header', 'settings', 'home']); + const PageObjects = getPageObjects(['common', 'header', 'settings', 'home', 'error']); class LoginPage { async login(username, password, options = {}) { @@ -27,6 +27,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const expectSpaceSelector = options.expectSpaceSelector || false; const expectSuccess = options.expectSuccess; + const expectForbidden = options.expectForbidden || false; await PageObjects.common.navigateToApp('login'); await testSubjects.setValue('loginUsername', username); @@ -37,6 +38,11 @@ export function SecurityPageProvider({ getService, getPageObjects }) { if (expectSpaceSelector) { await retry.try(() => testSubjects.find('kibanaSpaceSelector')); log.debug(`Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}`); + } else if (expectForbidden) { + await retry.try(async () => { + await PageObjects.error.expectForbidden(); + }); + log.debug(`Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectSuccess) { await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); @@ -73,7 +79,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async login(username, password, options = {}) { await this.loginPage.login(username, password, options); - if (options.expectSpaceSelector) { + if (options.expectSpaceSelector || options.expectForbidden) { return; } @@ -97,6 +103,22 @@ export function SecurityPageProvider({ getService, getPageObjects }) { )); } + async forceLogout() { + log.debug('SecurityPage.forceLogout'); + if (await find.existsByDisplayedByCssSelector('.login-form', 100)) { + log.debug('Already on the login page, not forcing anything'); + return; + } + + log.debug('Redirecting to /logout to force the logout'); + const url = PageObjects.common.getHostPort() + '/logout'; + await browser.get(url); + log.debug('Waiting on the login form to appear'); + await retry.waitForWithTimeout('login form', config.get('timeouts.waitFor') * 5, async () => ( + await find.existsByDisplayedByCssSelector('.login-form') + )); + } + async clickRolesSection() { await testSubjects.click('roles'); } @@ -273,17 +295,27 @@ export function SecurityPageProvider({ getService, getPageObjects }) { function addKibanaPriv(priv) { - return priv.reduce(function (promise, privName) { - // We have to use non-test-subject selectors because this markup is generated by ui-select. - return promise + return priv.reduce(async function (promise, privName) { - .then(async function () { - log.debug('priv item = ' + privName); - return find.byCssSelector(`[data-test-subj="kibanaMinimumPrivilege"] option[value="${privName}"]`); - }) - .then(function (element) { - return element.click(); - }); + const button = await testSubjects.find('addSpacePrivilegeButton'); + await button.click(); + + const spaceSelector = await testSubjects.find('spaceSelectorComboBox'); + await spaceSelector.click(); + + const globalSpaceOption = await find.byCssSelector(`#spaceOption_\\*`); + await globalSpaceOption.click(); + + const basePrivilegeSelector = await testSubjects.find('basePrivilegeComboBox'); + await basePrivilegeSelector.click(); + + const privilegeOption = await find.byCssSelector(`#basePrivilege_${privName}`); + await privilegeOption.click(); + + const createPrivilegeButton = await testSubjects.find('createSpacePrivilegeButton'); + await createPrivilegeButton.click(); + + return promise; }, Promise.resolve()); } diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json index 075951a0716450..0e1db569a2082d 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json +++ b/x-pack/test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/mappings.json @@ -199,6 +199,9 @@ "description": { "type": "text" }, + "disabledFeatures": { + "type": "keyword" + }, "initials": { "type": "keyword" }, @@ -322,4 +325,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js index 3fdbb6b9a2509f..8980bc565a2d35 100644 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js @@ -8,7 +8,7 @@ import mappings from './mappings.json'; export default function (kibana) { return new kibana.Plugin({ - require: [], + require: ['kibana', 'elasticsearch', 'xpack_main'], name: 'namespace_agnostic_type_plugin', uiExports: { savedObjectSchemas: { @@ -20,5 +20,31 @@ export default function (kibana) { }, config() {}, + + init(server) { + server.plugins.xpack_main.registerFeature({ + id: 'namespace_agnostic_type_plugin', + name: 'namespace_agnostic_type_plugin', + icon: 'upArrow', + navLinkId: 'namespace_agnostic_type_plugin', + app: [], + privileges: { + all: { + savedObject: { + all: ['globaltype'], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: ['globaltype'], + }, + ui: [], + } + } + }); + } }); } diff --git a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts index ab284d13f0d0af..730e974de43c73 100644 --- a/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/saved_object_api_integration/common/lib/create_users_and_roles.ts @@ -7,88 +7,128 @@ import { SuperTest } from 'supertest'; import { AUTHENTICATION } from './authentication'; export const createUsersAndRoles = async (es: any, supertest: SuperTest) => { - await supertest.put('/api/security/role/kibana_legacy_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_legacy_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_dual_privileges_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + kibana: [ { - names: ['.kibana'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['all'], + spaces: ['*'], }, ], - }, - }); - - await supertest.put('/api/security/role/kibana_dual_privileges_user').send({ - elasticsearch: { - indices: [ + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_dual_privileges_dashboard_only_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ { - names: ['.kibana'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['read'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['all'], - }, + }) + .expect(204); + + await supertest.put('/api/security/role/kibana_rbac_user').send({ + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], }); - await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_rbac_dashboard_only_user') + .send({ + kibana: [ { - names: ['.kibana'], - privileges: ['read', 'view_index_metadata'], + base: ['read'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['read'], - }, - }); - - await supertest.put('/api/security/role/kibana_rbac_user').send({ - kibana: { - global: ['all'], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user').send({ - kibana: { - global: ['read'], - }, - }); - - await supertest.put('/api/security/role/kibana_rbac_default_space_all_user').send({ - kibana: { - space: { - default: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_default_space_read_user').send({ - kibana: { - space: { - default: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_all_user').send({ - kibana: { - space: { - space_1: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_read_user').send({ - kibana: { - space: { - space_1: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); await es.shield.putUser({ username: AUTHENTICATION.NOT_A_KIBANA_USER.username, diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts index b0390af36017a1..4e9d367be5399d 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_create.ts @@ -136,7 +136,7 @@ export function bulkCreateTestSuiteFactory(es: any, esArchiver: any, supertest: expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype,visualization, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/visualization/bulk_create`, + message: `Unable to bulk_create dashboard,globaltype,visualization`, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts index 8d25d3cd0c776f..2d49d3ecbd6bd2 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_get.ts @@ -79,7 +79,7 @@ export function bulkGetTestSuiteFactory(esArchiver: any, supertest: SuperTest) }; const createExpectRbacForbidden = (type?: string) => (resp: { [key: string]: any }) => { - const message = type - ? `Unable to find ${type}, missing action:saved_objects/${type}/find` - : `Not authorized to find saved_object`; + const message = type ? `Unable to find ${type}` : `Not authorized to find saved_object`; expect(resp.body).to.eql({ statusCode: 403, diff --git a/x-pack/test/saved_object_api_integration/common/suites/get.ts b/x-pack/test/saved_object_api_integration/common/suites/get.ts index aeafe05d0b3c50..7e4340862fc7bf 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/get.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/get.ts @@ -53,7 +53,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) const createExpectNotSpaceAwareRbacForbidden = () => (resp: { [key: string]: any }) => { expect(resp.body).to.eql({ error: 'Forbidden', - message: `Unable to get globaltype, missing action:saved_objects/globaltype/get`, + message: `Unable to get globaltype`, statusCode: 403, }); }; @@ -76,7 +76,7 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperTest) const createExpectRbacForbidden = (type: string) => (resp: { [key: string]: any }) => { expect(resp.body).to.eql({ error: 'Forbidden', - message: `Unable to get ${type}, missing action:saved_objects/${type}/get`, + message: `Unable to get ${type}`, statusCode: 403, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/import.ts b/x-pack/test/saved_object_api_integration/common/suites/import.ts index 2e71d4a50f85e1..afb84fd5f93599 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/import.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/import.ts @@ -77,7 +77,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create`, + message: `Unable to bulk_create dashboard,globaltype`, }); }; @@ -85,7 +85,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/globaltype/bulk_create,action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create dashboard,globaltype,wigwags`, }); }; @@ -93,7 +93,7 @@ export function importTestSuiteFactory(es: any, esArchiver: any, supertest: Supe expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,globaltype,wigwags, missing action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create wigwags`, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts index 7419de868be536..a1048f251cb4fc 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/resolve_import_errors.ts @@ -81,7 +81,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard, missing action:saved_objects/dashboard/bulk_create`, + message: `Unable to bulk_create dashboard`, }); }; @@ -89,7 +89,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/dashboard/bulk_create,action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create dashboard,wigwags`, }); }; @@ -97,7 +97,7 @@ export function resolveImportErrorsTestSuiteFactory( expect(resp.body).to.eql({ statusCode: 403, error: 'Forbidden', - message: `Unable to bulk_create dashboard,wigwags, missing action:saved_objects/wigwags/bulk_create`, + message: `Unable to bulk_create wigwags`, }); }; diff --git a/x-pack/test/saved_object_api_integration/common/suites/update.ts b/x-pack/test/saved_object_api_integration/common/suites/update.ts index e292daa13d1225..fd72e4c20e2930 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/update.ts @@ -51,7 +51,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest) => { - await supertest.put('/api/security/role/kibana_legacy_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_legacy_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_dual_privileges_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['manage', 'read', 'index', 'delete'], + }, + ], + }, + kibana: [ { - names: ['.kibana*'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['all'], + spaces: ['*'], }, ], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_dual_privileges_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_dual_privileges_dashboard_only_user') + .send({ + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['read', 'view_index_metadata'], + }, + ], + }, + kibana: [ { - names: ['.kibana*'], - privileges: ['manage', 'read', 'index', 'delete'], + base: ['read'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['all'], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_dual_privileges_dashboard_only_user').send({ - elasticsearch: { - indices: [ + await supertest + .put('/api/security/role/kibana_rbac_user') + .send({ + kibana: [ { - names: ['.kibana*'], - privileges: ['read', 'view_index_metadata'], + base: ['all'], + spaces: ['*'], }, ], - }, - kibana: { - global: ['read'], - }, - }); - - await supertest.put('/api/security/role/kibana_rbac_user').send({ - kibana: { - global: ['all'], - }, - }); + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_dashboard_only_user').send({ - kibana: { - global: ['read'], - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_dashboard_only_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_default_space_all_user').send({ - kibana: { - space: { - default: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_default_space_read_user').send({ - kibana: { - space: { - default: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_default_space_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['default'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_all_user').send({ - kibana: { - space: { - space_1: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_read_user').send({ - kibana: { - space: { - space_1: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_1'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_2_all_user').send({ - kibana: { - space: { - space_2: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_2_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_2'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_2_read_user').send({ - kibana: { - space: { - space_2: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_2_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_2'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_2_all_user').send({ - kibana: { - space: { - space_1: ['all'], - space_2: ['all'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_2_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_1', 'space_2'], + }, + ], + }) + .expect(204); - await supertest.put('/api/security/role/kibana_rbac_space_1_2_read_user').send({ - kibana: { - space: { - space_1: ['read'], - space_2: ['read'], - }, - }, - }); + await supertest + .put('/api/security/role/kibana_rbac_space_1_2_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_1', 'space_2'], + }, + ], + }) + .expect(204); await es.shield.putUser({ username: AUTHENTICATION.NOT_A_KIBANA_USER.username, @@ -263,4 +320,44 @@ export const createUsersAndRoles = async (es: any, supertest: SuperTest) => email: 'a_kibana_rbac_space_1_2_readonly_user@elastic.co', }, }); + + await es.shield.putUser({ + username: AUTHENTICATION.APM_USER.username, + body: { + password: AUTHENTICATION.APM_USER.password, + roles: ['apm_user'], + full_name: 'a apm user', + email: 'a_apm_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MACHINE_LEARING_ADMIN.username, + body: { + password: AUTHENTICATION.MACHINE_LEARING_ADMIN.password, + roles: ['machine_learning_admin'], + full_name: 'a machine learning admin', + email: 'a_machine_learning_admin@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MACHINE_LEARNING_USER.username, + body: { + password: AUTHENTICATION.MACHINE_LEARNING_USER.password, + roles: ['machine_learning_user'], + full_name: 'a machine learning user', + email: 'a_machine_learning_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MONITORING_USER.username, + body: { + password: AUTHENTICATION.MONITORING_USER.password, + roles: ['monitoring_user'], + full_name: 'a monitoring user', + email: 'a_monitoring_user@elastic.co', + }, + }); }; diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index 6af5afc2530c76..4de638c784147d 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -40,6 +40,7 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) name: 'Default Space', description: 'This is the default space', _reserved: true, + disabledFeatures: [], }, { id: 'space_1', name: 'Space 1', description: 'This is the first test space', + disabledFeatures: [], }, { id: 'space_2', name: 'Space 2', description: 'This is the second test space', + disabledFeatures: [], }, ]; expect(resp.body).to.eql(allSpaces.find(space => space.id === spaceId)); diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 64acb60308c41a..ea5d391cfa0ca9 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -31,16 +31,19 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest spaceIds.includes(entry.id)); expect(resp.body).to.eql(expectedBody); diff --git a/x-pack/test/spaces_api_integration/common/suites/select.ts b/x-pack/test/spaces_api_integration/common/suites/select.ts index 489dc9f26529b1..ab8cbfcd0ddc29 100644 --- a/x-pack/test/spaces_api_integration/common/suites/select.ts +++ b/x-pack/test/spaces_api_integration/common/suites/select.ts @@ -55,17 +55,20 @@ export function selectTestSuiteFactory(esArchiver: any, supertest: SuperTest space.id === spaceId)); diff --git a/x-pack/test/spaces_api_integration/common/suites/update.ts b/x-pack/test/spaces_api_integration/common/suites/update.ts index 88297df3113349..5d64bee0f15a01 100644 --- a/x-pack/test/spaces_api_integration/common/suites/update.ts +++ b/x-pack/test/spaces_api_integration/common/suites/update.ts @@ -48,6 +48,7 @@ export function updateTestSuiteFactory(esArchiver: any, supertest: SuperTest { @@ -180,6 +188,50 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { }, } ); + + getAllTest(`apm_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.apmUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`machine_learning_admin can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.machineLearningAdmin, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`machine_learning_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.machineLearningUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`monitoring_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.monitoringUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); }); }); } diff --git a/x-pack/test/types/providers.ts b/x-pack/test/types/providers.ts index 2d655b8176807f..354adccd5f620f 100644 --- a/x-pack/test/types/providers.ts +++ b/x-pack/test/types/providers.ts @@ -13,4 +13,5 @@ export interface KibanaFunctionalTestDefaultProviders { getService(serviceName: string): any; getPageObjects(pageObjectNames: string[]): any; loadTestFile(path: string): void; + readConfigFile(path: string): any; } diff --git a/x-pack/test/types/services.d.ts b/x-pack/test/types/services.d.ts new file mode 100644 index 00000000000000..a219e43ebd9560 --- /dev/null +++ b/x-pack/test/types/services.d.ts @@ -0,0 +1,9 @@ +/* + * 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 LogService { + debug: (message: string) => void; +} diff --git a/x-pack/test/typings/index.d.ts b/x-pack/test/typings/index.d.ts new file mode 100644 index 00000000000000..0688ef9e4d8ec8 --- /dev/null +++ b/x-pack/test/typings/index.d.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +declare module '*.html' { + const template: string; + // eslint-disable-next-line import/no-default-export + export default template; +} diff --git a/x-pack/test/ui_capabilities/README.md b/x-pack/test/ui_capabilities/README.md new file mode 100644 index 00000000000000..b0f9f8eec7c8dc --- /dev/null +++ b/x-pack/test/ui_capabilities/README.md @@ -0,0 +1,48 @@ +# UI Capability Tests +These tests give us the most coverage to ensure that spaces and security work independently and cooperatively. They each cover different situations, and are supplemented by functional UI tests to ensure that security and spaces independently are able to disable the UI elements. These tests are using a "foo" plugin to ensure that its UI capabilities are adjusted appropriately. We aren't using actual plugins/apps for these tests, as they are prone to change and that's not the point of these tests. These tests are to ensure that the primary UI capabilities are adjusted appropriately by both the security and spaces plugins. + +## Security and Spaces + +We want to test for all combinations of the following users at the following spaces. The goal of these tests is to ensure that ui capabilities can be disabled by either the privileges at a specific space, or the space disabling the features. + +### Users +user with no kibana privileges +superuser +legacy all +legacy read +dual privileges all +dual privileges read +global read +global all +everything_space read +everything_space all +nothing_space read +nothing_space all + +### Spaces +everything_space - all features enabled +nothing_space - no features enabled + +## Security + +The security tests focus on more permutations of user's privileges, and focus primarily on privileges granted globally (at all spaces). + +### Users +no kibana privileges +superuser +legacy all +dual privileges all +dual privileges read +global read +global all +foo read +foo all + +## Spaces + +The Space tests focus on the result of disabling certain feature(s). + +### Spaces +everything enabled +nothing enabled +foo disabled diff --git a/x-pack/test/ui_capabilities/common/config.ts b/x-pack/test/ui_capabilities/common/config.ts new file mode 100644 index 00000000000000..bf770579ca1bd6 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/config.ts @@ -0,0 +1,57 @@ +/* + * 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 path from 'path'; +import { SecurityServiceProvider, SpacesServiceProvider } from '../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../types/providers'; +import { FeaturesProvider, UICapabilitiesProvider } from './services'; + +interface CreateTestConfigOptions { + license: string; + disabledPlugins?: string[]; +} + +// eslint-disable-next-line import/no-default-export +export function createTestConfig(name: string, options: CreateTestConfigOptions) { + const { license = 'trial', disabledPlugins = [] } = options; + + return async ({ readConfigFile }: KibanaFunctionalTestDefaultProviders) => { + const xPackFunctionalTestsConfig = await readConfigFile( + require.resolve('../../functional/config.js') + ); + + return { + testFiles: [require.resolve(`../${name}/tests/`)], + servers: xPackFunctionalTestsConfig.get('servers'), + services: { + security: SecurityServiceProvider, + spaces: SpacesServiceProvider, + uiCapabilities: UICapabilitiesProvider, + features: FeaturesProvider, + }, + junit: { + reportName: 'X-Pack UI Capabilities Functional Tests', + }, + esArchiver: {}, + esTestCluster: { + ...xPackFunctionalTestsConfig.get('esTestCluster'), + license, + serverArgs: [ + `xpack.license.self_generated.type=${license}`, + `xpack.security.enabled=${!disabledPlugins.includes('security') && license === 'trial'}`, + ], + }, + kbnTestServer: { + ...xPackFunctionalTestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), + ...disabledPlugins.map(key => `--xpack.${key}.enabled=false`), + `--plugin-path=${path.join(__dirname, 'fixtures', 'plugins', 'foo_plugin')}`, + ], + }, + }; + }; +} diff --git a/x-pack/plugins/security/common/model/kibana_privilege.ts b/x-pack/test/ui_capabilities/common/features.ts similarity index 65% rename from x-pack/plugins/security/common/model/kibana_privilege.ts rename to x-pack/test/ui_capabilities/common/features.ts index 20cac65b4ca792..3c015bc21e9377 100644 --- a/x-pack/plugins/security/common/model/kibana_privilege.ts +++ b/x-pack/test/ui_capabilities/common/features.ts @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -export type KibanaPrivilege = 'none' | 'read' | 'all'; +interface Feature { + navLinkId: string; +} -export const KibanaAppPrivileges: KibanaPrivilege[] = ['read', 'all']; +export interface Features { + [key: string]: Feature; +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js new file mode 100644 index 00000000000000..07c39d34b13090 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js @@ -0,0 +1,48 @@ +/* + * 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 default function (kibana) { + return new kibana.Plugin({ + require: ['kibana', 'elasticsearch', 'xpack_main'], + name: 'foo', + uiExports: { + app: { + title: 'Foo', + order: 1000, + euiIconType: 'uiArray', + description: 'Foo app', + main: 'plugins/foo_plugin/app', + }, + }, + + init(server) { + server.plugins.xpack_main.registerFeature({ + id: 'foo', + name: 'Foo', + icon: 'upArrow', + navLinkId: 'foo_plugin', + app: ['kibana'], + catalogue: ['foo'], + privileges: { + all: { + savedObject: { + all: ['foo'], + read: ['index-pattern', 'config'], + }, + ui: ['create', 'edit', 'delete', 'show'], + }, + read: { + savedObject: { + all: [], + read: ['foo', 'index-pattern', 'config'], + }, + ui: ['show'], + } + } + }); + } + }); +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json new file mode 100644 index 00000000000000..45228b47c0a318 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json @@ -0,0 +1,7 @@ +{ + "name": "foo_plugin", + "version": "0.0.0", + "kibana": { + "version": "kibana" + } +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js new file mode 100644 index 00000000000000..41bc2aa2588073 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js @@ -0,0 +1,5 @@ +/* + * 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. + */ diff --git a/x-pack/test/ui_capabilities/common/lib/unreachable_error.ts b/x-pack/test/ui_capabilities/common/lib/unreachable_error.ts new file mode 100644 index 00000000000000..93e06a89ea05fe --- /dev/null +++ b/x-pack/test/ui_capabilities/common/lib/unreachable_error.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +class UnreachableError extends Error { + constructor(val: never) { + super(`Unreachable: ${val}`); + } +} diff --git a/x-pack/test/ui_capabilities/common/nav_links_builder.ts b/x-pack/test/ui_capabilities/common/nav_links_builder.ts new file mode 100644 index 00000000000000..8b7741469362e8 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/nav_links_builder.ts @@ -0,0 +1,44 @@ +/* + * 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 { Features } from './features'; + +type buildCallback = (featureId: string) => boolean; +export class NavLinksBuilder { + private readonly features: Features; + constructor(features: Features) { + this.features = { + ...features, + // management isn't a first-class "feature", but it makes our life easier here to pretend like it is + management: { + navLinkId: 'kibana:management', + }, + }; + } + + public all() { + return this.build(() => true); + } + public except(...feature: string[]) { + return this.build(featureId => !feature.includes(featureId)); + } + public none() { + return this.build(() => false); + } + public only(...feature: string[]) { + return this.build(featureId => feature.includes(featureId)); + } + + private build(callback: buildCallback): Record { + const navLinks = {} as Record; + for (const [featureId, feature] of Object.entries(this.features)) { + if (feature.navLinkId) { + navLinks[feature.navLinkId] = callback(featureId); + } + } + + return navLinks; + } +} diff --git a/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts new file mode 100644 index 00000000000000..c522d68180fd36 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/saved_objects_management_builder.ts @@ -0,0 +1,88 @@ +/* + * 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. + */ + +/* eslint-disable max-classes-per-file */ +class SavedObjectsTypeUICapabilitiesGroup { + public all = ['delete', 'edit', 'read']; + public read = ['read']; +} +const savedObjectsTypeUICapabilitiesGroup = new SavedObjectsTypeUICapabilitiesGroup(); + +interface OnlyParameters { + all?: string | string[]; + read?: string | string[]; +} + +const coerceToArray = (itemOrItemsOrNil: T | T[] | undefined): T[] => { + if (itemOrItemsOrNil == null) { + return []; + } + + return Array.isArray(itemOrItemsOrNil) ? itemOrItemsOrNil : [itemOrItemsOrNil]; +}; + +export class SavedObjectsManagementBuilder { + private allSavedObjectTypes: string[]; + + constructor(spacesEnabled: boolean) { + this.allSavedObjectTypes = [ + ...(spacesEnabled ? ['space'] : []), + 'config', + 'telemetry', + 'graph-workspace', + 'ml-telemetry', + 'apm-telemetry', + 'map', + 'maps-telemetry', + 'canvas-workpad', + 'infrastructure-ui-source', + 'upgrade-assistant-reindex-operation', + 'upgrade-assistant-telemetry', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'url', + 'server', + 'kql-telemetry', + 'timelion-sheet', + 'ui-metric', + 'sample-data-telemetry', + ]; + } + + public uiCapabilities(group: keyof SavedObjectsTypeUICapabilitiesGroup) { + return savedObjectsTypeUICapabilitiesGroup.all.reduce( + (acc2, uiCapability) => ({ + ...acc2, + [uiCapability]: savedObjectsTypeUICapabilitiesGroup[group].includes(uiCapability), + }), + {} + ); + } + + public build(parameters: OnlyParameters): Record { + const readTypes = coerceToArray(parameters.read); + const allTypes = coerceToArray(parameters.all); + return this.allSavedObjectTypes.reduce( + (acc, savedObjectType) => ({ + ...acc, + [savedObjectType]: savedObjectsTypeUICapabilitiesGroup.all.reduce( + (acc2, uiCapability) => ({ + ...acc2, + [uiCapability]: + (readTypes.includes(savedObjectType) && + savedObjectsTypeUICapabilitiesGroup.read.includes(uiCapability)) || + (allTypes.includes(savedObjectType) && + savedObjectsTypeUICapabilitiesGroup.all.includes(uiCapability)), + }), + {} + ), + }), + {} + ); + } +} diff --git a/x-pack/test/ui_capabilities/common/services/features.ts b/x-pack/test/ui_capabilities/common/services/features.ts new file mode 100644 index 00000000000000..571dd901f1c067 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/services/features.ts @@ -0,0 +1,56 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import { format as formatUrl } from 'url'; +import util from 'util'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; +import { Features } from '../features'; + +export class FeaturesService { + private readonly axios: AxiosInstance; + + constructor(url: string, private readonly log: LogService) { + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/features' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we'll handle our own statusCodes and throw informative errors + }); + } + + public async get(): Promise { + this.log.debug(`requesting /api/features/v1 to get the features`); + const response = await this.axios.get('/api/features/v1'); + + if (response.status !== 200) { + throw new Error( + `Expected status code of 200, received ${response.status} ${ + response.statusText + }: ${util.inspect(response.data)}` + ); + } + + const features = response.data.reduce( + (acc: Features, feature: any) => ({ + ...acc, + [feature.id]: { + navLinkId: feature.navLinkId, + }, + }), + {} + ); + return features; + } +} + +export function FeaturesProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const url = formatUrl(config.get('servers.kibana')); + + return new FeaturesService(url, log); +} diff --git a/x-pack/test/ui_capabilities/common/services/index.ts b/x-pack/test/ui_capabilities/common/services/index.ts new file mode 100644 index 00000000000000..9fe948d509eed1 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/services/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { FeaturesProvider, FeaturesService } from './features'; +export { UICapabilitiesProvider } from './ui_capabilities'; diff --git a/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts new file mode 100644 index 00000000000000..970519e61cc82c --- /dev/null +++ b/x-pack/test/ui_capabilities/common/services/ui_capabilities.ts @@ -0,0 +1,114 @@ +/* + * 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 axios, { AxiosInstance } from 'axios'; +import cheerio from 'cheerio'; +import { UICapabilities } from 'ui/capabilities'; +import { format as formatUrl } from 'url'; +import util from 'util'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { LogService } from '../../../types/services'; + +export interface BasicCredentials { + username: string; + password: string; +} + +export enum GetUICapabilitiesFailureReason { + RedirectedToRoot = 'Redirected to Root', + NotFound = 'Not Found', +} + +interface GetUICapabilitiesResult { + success: boolean; + value?: UICapabilities; + failureReason?: GetUICapabilitiesFailureReason; +} + +export class UICapabilitiesService { + private readonly log: LogService; + private readonly axios: AxiosInstance; + + constructor(url: string, log: LogService) { + this.log = log; + this.axios = axios.create({ + headers: { 'kbn-xsrf': 'x-pack/ftr/services/ui_capabilities' }, + baseURL: url, + maxRedirects: 0, + validateStatus: () => true, // we'll handle our own statusCodes and throw informative errors + }); + } + + public async get( + credentials: BasicCredentials | null, + spaceId?: string + ): Promise { + const spaceUrlPrefix = spaceId ? `/s/${spaceId}` : ''; + this.log.debug(`requesting ${spaceUrlPrefix}/app/kibana to parse the uiCapabilities`); + const requestHeaders = credentials + ? { + Authorization: `Basic ${Buffer.from( + `${credentials.username}:${credentials.password}` + ).toString('base64')}`, + } + : {}; + const response = await this.axios.get(`${spaceUrlPrefix}/app/kibana`, { + headers: requestHeaders, + }); + + if (response.status === 302 && response.headers.location === '/') { + return { + success: false, + failureReason: GetUICapabilitiesFailureReason.RedirectedToRoot, + }; + } + + if (response.status === 404) { + return { + success: false, + failureReason: GetUICapabilitiesFailureReason.NotFound, + }; + } + + if (response.status !== 200) { + throw new Error( + `Expected status code of 200, received ${response.status} ${ + response.statusText + }: ${util.inspect(response.data)}` + ); + } + + const dom = cheerio.load(response.data.toString()); + const element = dom('kbn-injected-metadata'); + if (!element) { + throw new Error('Unable to find "kbn-injected-metadata" element '); + } + + const dataAttrJson = element.attr('data'); + + try { + const dataAttr = JSON.parse(dataAttrJson); + return { + success: true, + value: dataAttr.vars.uiCapabilities as UICapabilities, + }; + } catch (err) { + throw new Error( + `Unable to parse JSON from the kbn-injected-metadata data attribute: ${dataAttrJson}` + ); + } + } +} + +export function UICapabilitiesProvider({ getService }: KibanaFunctionalTestDefaultProviders) { + const log = getService('log'); + const config = getService('config'); + const noAuthUrl = formatUrl({ + ...config.get('servers.kibana'), + auth: undefined, + }); + + return new UICapabilitiesService(noAuthUrl, log); +} diff --git a/x-pack/test/ui_capabilities/common/types.ts b/x-pack/test/ui_capabilities/common/types.ts new file mode 100644 index 00000000000000..d7f34b9ecff088 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/types.ts @@ -0,0 +1,56 @@ +/* + * 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 FeaturesPrivileges { + [featureId: string]: string[]; +} + +// TODO: Consolidate the following type definitions +interface CustomRoleSpecificationElasticsearchIndices { + names: string[]; + privileges: string[]; +} + +export interface RoleKibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface CustomRoleSpecification { + name: string; + elasticsearch?: { + cluster: string[]; + indices: CustomRoleSpecificationElasticsearchIndices[]; + }; + kibana?: RoleKibanaPrivilege[]; +} + +interface ReservedRoleSpecification { + name: string; +} + +export function isCustomRoleSpecification( + roleSpecification: CustomRoleSpecification | ReservedRoleSpecification +): roleSpecification is CustomRoleSpecification { + const customRoleDefinition = roleSpecification as CustomRoleSpecification; + return ( + customRoleDefinition.kibana !== undefined || customRoleDefinition.elasticsearch !== undefined + ); +} + +export interface User { + username: string; + fullName: string; + password: string; + role?: ReservedRoleSpecification | CustomRoleSpecification; + roles?: Array; +} + +export interface Space { + id: string; + name: string; + disabledFeatures: string[] | '*'; +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/config.ts b/x-pack/test/ui_capabilities/security_and_spaces/config.ts new file mode 100644 index 00000000000000..9fdbf8f47baabc --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/config.ts @@ -0,0 +1,10 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_and_spaces', { disabledPlugins: [], license: 'trial' }); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts b/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts new file mode 100644 index 00000000000000..8e1289df364c09 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/scenarios.ts @@ -0,0 +1,468 @@ +/* + * 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 { Space, User } from '../common/types'; + +const NoKibanaPrivileges: User = { + username: 'no_kibana_privileges', + fullName: 'no_kibana_privileges', + password: 'no_kibana_privileges-password', + role: { + name: 'no_kibana_privileges', + elasticsearch: { + indices: [ + { + names: ['foo'], + privileges: ['all'], + }, + ], + }, + }, +}; + +const Superuser: User = { + username: 'superuser', + fullName: 'superuser', + password: 'superuser-password', + role: { + name: 'superuser', + }, +}; + +const LegacyAll: User = { + username: 'legacy_all', + fullName: 'legacy_all', + password: 'legacy_all-password', + role: { + name: 'legacy_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +const DualPrivilegesAll: User = { + username: 'dual_privileges_all', + fullName: 'dual_privileges_all', + password: 'dual_privileges_all-password', + role: { + name: 'dual_privileges_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, +}; + +const DualPrivilegesRead: User = { + username: 'dual_privileges_read', + fullName: 'dual_privileges_read', + password: 'dual_privileges_read-password', + role: { + name: 'dual_privileges_read_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +const GlobalAll: User = { + username: 'global_all', + fullName: 'global_all', + password: 'global_all-password', + role: { + name: 'global_all_role', + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, +}; + +const GlobalRead: User = { + username: 'global_read', + fullName: 'global_read', + password: 'global_read-password', + role: { + name: 'global_read_role', + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +const EverythingSpaceAll: User = { + username: 'everything_space_all', + fullName: 'everything_space_all', + password: 'everything_space_all-password', + role: { + name: 'everything_space_all_role', + kibana: [ + { + base: ['all'], + spaces: ['everything_space'], + }, + ], + }, +}; + +const EverythingSpaceRead: User = { + username: 'everything_space_read', + fullName: 'everything_space_read', + password: 'everything_space_read-password', + role: { + name: 'everything_space_read_role', + kibana: [ + { + base: ['read'], + spaces: ['everything_space'], + }, + ], + }, +}; + +const NothingSpaceAll: User = { + username: 'nothing_space_all', + fullName: 'nothing_space_all', + password: 'nothing_space_all-password', + role: { + name: 'nothing_space_all_role', + kibana: [ + { + base: ['all'], + spaces: ['nothing_space'], + }, + ], + }, +}; + +const NothingSpaceRead: User = { + username: 'nothing_space_read', + fullName: 'nothing_space_read', + password: 'nothing_space_read-password', + role: { + name: 'nothing_space_read_role', + kibana: [ + { + base: ['read'], + spaces: ['nothing_space'], + }, + ], + }, +}; + +export const Users: User[] = [ + NoKibanaPrivileges, + Superuser, + LegacyAll, + DualPrivilegesAll, + DualPrivilegesRead, + GlobalAll, + GlobalRead, + EverythingSpaceAll, + EverythingSpaceRead, + NothingSpaceAll, + NothingSpaceRead, +]; + +const EverythingSpace: Space = { + id: 'everything_space', + name: 'everything_space', + disabledFeatures: [], +}; + +const NothingSpace: Space = { + id: 'nothing_space', + name: 'nothing_space', + disabledFeatures: '*', +}; + +export const Spaces: Space[] = [EverythingSpace, NothingSpace]; + +// For all scenarios, we define both an instance in addition +// to a "type" definition so that we can use the exhaustive switch in +// typescript to ensure all scenarios are handled. + +interface Scenario { + user: User; + space: Space; +} + +interface NoKibanaPrivilegesAtEverythingSpace extends Scenario { + id: 'no_kibana_privileges at everything_space'; +} +const NoKibanaPrivilegesAtEverythingSpace: NoKibanaPrivilegesAtEverythingSpace = { + id: 'no_kibana_privileges at everything_space', + user: NoKibanaPrivileges, + space: EverythingSpace, +}; + +interface NoKibanaPrivilegesAtNothingSpace extends Scenario { + id: 'no_kibana_privileges at nothing_space'; +} +const NoKibanaPrivilegesAtNothingSpace: NoKibanaPrivilegesAtNothingSpace = { + id: 'no_kibana_privileges at nothing_space', + user: NoKibanaPrivileges, + space: NothingSpace, +}; + +interface SuperuserAtEverythingSpace extends Scenario { + id: 'superuser at everything_space'; +} +const SuperuserAtEverythingSpace: SuperuserAtEverythingSpace = { + id: 'superuser at everything_space', + user: Superuser, + space: EverythingSpace, +}; + +interface SuperuserAtNothingSpace extends Scenario { + id: 'superuser at nothing_space'; +} +const SuperuserAtNothingSpace: SuperuserAtNothingSpace = { + id: 'superuser at nothing_space', + user: Superuser, + space: NothingSpace, +}; + +interface LegacyAllAtEverythingSpace extends Scenario { + id: 'legacy_all at everything_space'; +} +const LegacyAllAtEverythingSpace: LegacyAllAtEverythingSpace = { + id: 'legacy_all at everything_space', + user: LegacyAll, + space: EverythingSpace, +}; + +interface LegacyAllAtNothingSpace extends Scenario { + id: 'legacy_all at nothing_space'; +} +const LegacyAllAtNothingSpace: LegacyAllAtNothingSpace = { + id: 'legacy_all at nothing_space', + user: LegacyAll, + space: NothingSpace, +}; + +interface DualPrivilegesAllAtEverythingSpace extends Scenario { + id: 'dual_privileges_all at everything_space'; +} +const DualPrivilegesAllAtEverythingSpace: DualPrivilegesAllAtEverythingSpace = { + id: 'dual_privileges_all at everything_space', + user: DualPrivilegesAll, + space: EverythingSpace, +}; + +interface DualPrivilegesAllAtNothingSpace extends Scenario { + id: 'dual_privileges_all at nothing_space'; +} +const DualPrivilegesAllAtNothingSpace: DualPrivilegesAllAtNothingSpace = { + id: 'dual_privileges_all at nothing_space', + user: DualPrivilegesAll, + space: NothingSpace, +}; + +interface DualPrivilegesReadAtEverythingSpace extends Scenario { + id: 'dual_privileges_read at everything_space'; +} +const DualPrivilegesReadAtEverythingSpace: DualPrivilegesReadAtEverythingSpace = { + id: 'dual_privileges_read at everything_space', + user: DualPrivilegesRead, + space: EverythingSpace, +}; + +interface DualPrivilegesReadAtNothingSpace extends Scenario { + id: 'dual_privileges_read at nothing_space'; +} +const DualPrivilegesReadAtNothingSpace: DualPrivilegesReadAtNothingSpace = { + id: 'dual_privileges_read at nothing_space', + user: DualPrivilegesRead, + space: NothingSpace, +}; + +interface GlobalAllAtEverythingSpace extends Scenario { + id: 'global_all at everything_space'; +} +const GlobalAllAtEverythingSpace: GlobalAllAtEverythingSpace = { + id: 'global_all at everything_space', + user: GlobalAll, + space: EverythingSpace, +}; + +interface GlobalAllAtNothingSpace extends Scenario { + id: 'global_all at nothing_space'; +} +const GlobalAllAtNothingSpace: GlobalAllAtNothingSpace = { + id: 'global_all at nothing_space', + user: GlobalAll, + space: NothingSpace, +}; + +interface GlobalReadAtEverythingSpace extends Scenario { + id: 'global_read at everything_space'; +} +const GlobalReadAtEverythingSpace: GlobalReadAtEverythingSpace = { + id: 'global_read at everything_space', + user: GlobalRead, + space: EverythingSpace, +}; + +interface GlobalReadAtNothingSpace extends Scenario { + id: 'global_read at nothing_space'; +} +const GlobalReadAtNothingSpace: GlobalReadAtNothingSpace = { + id: 'global_read at nothing_space', + user: GlobalRead, + space: NothingSpace, +}; + +interface EverythingSpaceAllAtEverythingSpace extends Scenario { + id: 'everything_space_all at everything_space'; +} +const EverythingSpaceAllAtEverythingSpace: EverythingSpaceAllAtEverythingSpace = { + id: 'everything_space_all at everything_space', + user: EverythingSpaceAll, + space: EverythingSpace, +}; + +interface EverythingSpaceAllAtNothingSpace extends Scenario { + id: 'everything_space_all at nothing_space'; +} +const EverythingSpaceAllAtNothingSpace: EverythingSpaceAllAtNothingSpace = { + id: 'everything_space_all at nothing_space', + user: EverythingSpaceAll, + space: NothingSpace, +}; + +interface EverythingSpaceReadAtEverythingSpace extends Scenario { + id: 'everything_space_read at everything_space'; +} +const EverythingSpaceReadAtEverythingSpace: EverythingSpaceReadAtEverythingSpace = { + id: 'everything_space_read at everything_space', + user: EverythingSpaceRead, + space: EverythingSpace, +}; + +interface EverythingSpaceReadAtNothingSpace extends Scenario { + id: 'everything_space_read at nothing_space'; +} +const EverythingSpaceReadAtNothingSpace: EverythingSpaceReadAtNothingSpace = { + id: 'everything_space_read at nothing_space', + user: EverythingSpaceRead, + space: NothingSpace, +}; + +interface NothingSpaceAllAtEverythingSpace extends Scenario { + id: 'nothing_space_all at everything_space'; +} +const NothingSpaceAllAtEverythingSpace: NothingSpaceAllAtEverythingSpace = { + id: 'nothing_space_all at everything_space', + user: NothingSpaceAll, + space: EverythingSpace, +}; + +interface NothingSpaceAllAtNothingSpace extends Scenario { + id: 'nothing_space_all at nothing_space'; +} +const NothingSpaceAllAtNothingSpace: NothingSpaceAllAtNothingSpace = { + id: 'nothing_space_all at nothing_space', + user: NothingSpaceAll, + space: NothingSpace, +}; + +interface NothingSpaceReadAtEverythingSpace extends Scenario { + id: 'nothing_space_read at everything_space'; +} +const NothingSpaceReadAtEverythingSpace: NothingSpaceReadAtEverythingSpace = { + id: 'nothing_space_read at everything_space', + user: NothingSpaceRead, + space: EverythingSpace, +}; + +interface NothingSpaceReadAtNothingSpace extends Scenario { + id: 'nothing_space_read at nothing_space'; +} +const NothingSpaceReadAtNothingSpace: NothingSpaceReadAtNothingSpace = { + id: 'nothing_space_read at nothing_space', + user: NothingSpaceRead, + space: NothingSpace, +}; + +export const UserAtSpaceScenarios: [ + NoKibanaPrivilegesAtEverythingSpace, + NoKibanaPrivilegesAtNothingSpace, + SuperuserAtEverythingSpace, + SuperuserAtNothingSpace, + LegacyAllAtEverythingSpace, + LegacyAllAtNothingSpace, + DualPrivilegesAllAtEverythingSpace, + DualPrivilegesAllAtNothingSpace, + DualPrivilegesReadAtEverythingSpace, + DualPrivilegesReadAtNothingSpace, + GlobalAllAtEverythingSpace, + GlobalAllAtNothingSpace, + GlobalReadAtEverythingSpace, + GlobalReadAtNothingSpace, + EverythingSpaceAllAtEverythingSpace, + EverythingSpaceAllAtNothingSpace, + EverythingSpaceReadAtEverythingSpace, + EverythingSpaceReadAtNothingSpace, + NothingSpaceAllAtEverythingSpace, + NothingSpaceAllAtNothingSpace, + NothingSpaceReadAtEverythingSpace, + NothingSpaceReadAtNothingSpace +] = [ + NoKibanaPrivilegesAtEverythingSpace, + NoKibanaPrivilegesAtNothingSpace, + SuperuserAtEverythingSpace, + SuperuserAtNothingSpace, + LegacyAllAtEverythingSpace, + LegacyAllAtNothingSpace, + DualPrivilegesAllAtEverythingSpace, + DualPrivilegesAllAtNothingSpace, + DualPrivilegesReadAtEverythingSpace, + DualPrivilegesReadAtNothingSpace, + GlobalAllAtEverythingSpace, + GlobalAllAtNothingSpace, + GlobalReadAtEverythingSpace, + GlobalReadAtNothingSpace, + EverythingSpaceAllAtEverythingSpace, + EverythingSpaceAllAtNothingSpace, + EverythingSpaceReadAtEverythingSpace, + EverythingSpaceReadAtNothingSpace, + NothingSpaceAllAtEverythingSpace, + NothingSpaceAllAtNothingSpace, + NothingSpaceReadAtEverythingSpace, + NothingSpaceReadAtNothingSpace, +]; diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts new file mode 100644 index 00000000000000..6972ede00abe3c --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('catalogue', () => { + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + case 'superuser at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'everything_space_all at everything_space': + case 'global_read at everything_space': + case 'dual_privileges_read at everything_space': + case 'everything_space_read at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // the nothing_space has no features enabled, so even if we have + // privileges to perform these actions, we won't be able to + case 'superuser at nothing_space': + case 'global_all at nothing_space': + case 'global_read at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'dual_privileges_read at nothing_space': + case 'nothing_space_all at nothing_space': + case 'nothing_space_read at nothing_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is disabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // if we don't have access at the space itself, we're + // redirected to the space selector and the ui capabilities + // are lagely irrelevant because they won't be consumed + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts new file mode 100644 index 00000000000000..cf0731769ac2ca --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/foo.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('foo', () => { + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + // these users have a read/write view + case 'superuser at everything_space': + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'everything_space_all at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: true, + edit: true, + delete: true, + show: true, + }); + break; + // these users have a read only view + case 'global_read at everything_space': + case 'dual_privileges_read at everything_space': + case 'everything_space_read at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: true, + }); + break; + // the nothing_space has no features enabled, so even if we have + // privileges to perform these actions, we won't be able to + case 'superuser at nothing_space': + case 'global_all at nothing_space': + case 'global_read at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'dual_privileges_read at nothing_space': + case 'nothing_space_all at nothing_space': + case 'nothing_space_read at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: false, + }); + break; + // if we don't have access at the space itself, we're + // redirected to the space selector and the ui capabilities + // are largely irrelevant because they won't be consumed + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts new file mode 100644 index 00000000000000..840fbecb18d962 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts @@ -0,0 +1,78 @@ +/* + * 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 { SpacesService } from '../../../common/services'; +import { SecurityService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { FeaturesService } from '../../common/services'; +import { isCustomRoleSpecification } from '../../common/types'; +import { Spaces, Users } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitiesTests({ + loadTestFile, + getService, +}: KibanaFunctionalTestDefaultProviders) { + const securityService: SecurityService = getService('security'); + const spacesService: SpacesService = getService('spaces'); + const featuresService: FeaturesService = getService('features'); + + describe('ui capabilities', function() { + this.tags('ciGroup5'); + + before(async () => { + const features = await featuresService.get(); + for (const space of Spaces) { + const disabledFeatures = + space.disabledFeatures === '*' ? Object.keys(features) : space.disabledFeatures; + await spacesService.create({ + ...space, + disabledFeatures, + }); + } + + for (const user of Users) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + + await securityService.user.create(user.username, { + password: user.password, + full_name: user.fullName, + roles: roles.map(role => role.name), + }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + }); + } + } + } + }); + + after(async () => { + for (const space of Spaces) { + await spacesService.delete(space.id); + } + + for (const user of Users) { + await securityService.user.delete(user.username); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } + } + } + }); + + loadTestFile(require.resolve('./catalogue')); + loadTestFile(require.resolve('./foo')); + loadTestFile(require.resolve('./nav_links')); + loadTestFile(require.resolve('./saved_objects_management')); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts new file mode 100644 index 00000000000000..7bbf39f11847cc --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.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 expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { NavLinksBuilder } from '../../common/nav_links_builder'; +import { FeaturesService } from '../../common/services'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function navLinksTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const featuresService: FeaturesService = getService('features'); + + describe('navLinks', () => { + let navLinksBuilder: NavLinksBuilder; + before(async () => { + const features = await featuresService.get(); + navLinksBuilder = new NavLinksBuilder(features); + }); + + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + case 'superuser at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'dual_privileges_read at everything_space': + case 'global_read at everything_space': + case 'everything_space_all at everything_space': + case 'everything_space_read at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); + break; + case 'superuser at nothing_space': + case 'global_all at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'dual_privileges_read at nothing_space': + case 'global_read at nothing_space': + case 'nothing_space_all at nothing_space': + case 'nothing_space_read at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management')); + break; + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts new file mode 100644 index 00000000000000..e2380e592adb27 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/saved_objects_management.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserAtSpaceScenarios } from '../scenarios'; + +const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(true); + +// eslint-disable-next-line import/no-default-export +export default function savedObjectsManagementTests({ + getService, +}: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('savedObjectsManagement', () => { + UserAtSpaceScenarios.forEach(scenario => { + it(`${scenario.id}`, async () => { + const { user, space } = scenario; + + const uiCapabilities = await uiCapabilitiesService.get( + { username: user.username, password: user.password }, + space.id + ); + switch (scenario.id) { + case 'superuser at everything_space': + case 'superuser at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () => + savedObjectsManagementBuilder.uiCapabilities('all') + ); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql(expected); + break; + case 'global_all at everything_space': + case 'dual_privileges_all at everything_space': + case 'everything_space_all at everything_space': + case 'global_all at nothing_space': + case 'dual_privileges_all at nothing_space': + case 'nothing_space_all at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + all: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'dual_privileges_read at everything_space': + case 'global_read at everything_space': + case 'everything_space_read at everything_space': + case 'dual_privileges_read at nothing_space': + case 'global_read at nothing_space': + case 'nothing_space_read at nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + read: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'no_kibana_privileges at everything_space': + case 'no_kibana_privileges at nothing_space': + case 'legacy_all at everything_space': + case 'legacy_all at nothing_space': + case 'everything_space_all at nothing_space': + case 'everything_space_read at nothing_space': + case 'nothing_space_all at everything_space': + case 'nothing_space_read at everything_space': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be( + GetUICapabilitiesFailureReason.RedirectedToRoot + ); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/config.ts b/x-pack/test/ui_capabilities/security_only/config.ts new file mode 100644 index 00000000000000..6b32ccc1b70d98 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/config.ts @@ -0,0 +1,10 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_only', { disabledPlugins: ['spaces'], license: 'trial' }); diff --git a/x-pack/test/ui_capabilities/security_only/scenarios.ts b/x-pack/test/ui_capabilities/security_only/scenarios.ts new file mode 100644 index 00000000000000..8f7e483417b5fb --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/scenarios.ts @@ -0,0 +1,215 @@ +/* + * 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 { CustomRoleSpecification, User } from '../common/types'; + +// For all scenarios, we define both an instance in addition +// to a "type" definition so that we can use the exhaustive switch in +// typescript to ensure all scenarios are handled. + +const allRole: CustomRoleSpecification = { + name: 'all_role', + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], +}; + +interface NoKibanaPrivileges extends User { + username: 'no_kibana_privileges'; +} +const NoKibanaPrivileges: NoKibanaPrivileges = { + username: 'no_kibana_privileges', + fullName: 'no_kibana_privileges', + password: 'no_kibana_privileges-password', + role: { + name: 'no_kibana_privileges', + elasticsearch: { + indices: [ + { + names: ['foo'], + privileges: ['all'], + }, + ], + }, + }, +}; + +interface Superuser extends User { + username: 'superuser'; +} +const Superuser: Superuser = { + username: 'superuser', + fullName: 'superuser', + password: 'superuser-password', + role: { + name: 'superuser', + }, +}; + +interface LegacyAll extends User { + username: 'legacy_all'; +} +const LegacyAll: LegacyAll = { + username: 'legacy_all', + fullName: 'legacy_all', + password: 'legacy_all-password', + role: { + name: 'legacy_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + }, +}; + +interface DualPrivilegesAll extends User { + username: 'dual_privileges_all'; +} +const DualPrivilegesAll: DualPrivilegesAll = { + username: 'dual_privileges_all', + fullName: 'dual_privileges_all', + password: 'dual_privileges_all-password', + role: { + name: 'dual_privileges_all_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }, +}; + +interface DualPrivilegesRead extends User { + username: 'dual_privileges_read'; +} +const DualPrivilegesRead: DualPrivilegesRead = { + username: 'dual_privileges_read', + fullName: 'dual_privileges_read', + password: 'dual_privileges_read-password', + role: { + name: 'dual_privileges_read_role', + elasticsearch: { + indices: [ + { + names: ['.kibana*'], + privileges: ['read'], + }, + ], + }, + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +interface All extends User { + username: 'all'; +} +const All: All = { + username: 'all', + fullName: 'all', + password: 'all-password', + role: allRole, +}; + +interface Read extends User { + username: 'read'; +} +const Read: Read = { + username: 'read', + fullName: 'read', + password: 'read-password', + role: { + name: 'read_role', + kibana: [ + { + base: ['read'], + spaces: ['*'], + }, + ], + }, +}; + +interface FooAll extends User { + username: 'foo_all'; +} +const FooAll: FooAll = { + username: 'foo_all', + fullName: 'foo_all', + password: 'foo_all-password', + role: { + name: 'foo_all_role', + kibana: [ + { + feature: { + foo: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +interface FooRead extends User { + username: 'foo_read'; +} +const FooRead: FooRead = { + username: 'foo_read', + fullName: 'foo_read', + password: 'foo_read-password', + role: { + name: 'foo_read_role', + kibana: [ + { + feature: { + foo: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const UserScenarios: [ + NoKibanaPrivileges, + Superuser, + LegacyAll, + DualPrivilegesAll, + DualPrivilegesRead, + All, + Read, + FooAll, + FooRead +] = [ + NoKibanaPrivileges, + Superuser, + LegacyAll, + DualPrivilegesAll, + DualPrivilegesRead, + All, + Read, + FooAll, + FooRead, +]; diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts new file mode 100644 index 00000000000000..fe4eca19fe8e82 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('catalogue', () => { + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + case 'superuser': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'all': + case 'read': + case 'dual_privileges_all': + case 'dual_privileges_read': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'foo_all': + case 'foo_read': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // only foo is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (value, catalogueId) => catalogueId === 'foo' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // these users have no access to even get the ui capabilities + case 'legacy_all': + case 'no_kibana_privileges': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/foo.ts b/x-pack/test/ui_capabilities/security_only/tests/foo.ts new file mode 100644 index 00000000000000..ab6090d17bb16b --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/foo.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('foo', () => { + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + // these users have a read/write view of Foo + case 'superuser': + case 'all': + case 'dual_privileges_all': + case 'foo_all': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: true, + edit: true, + delete: true, + show: true, + }); + break; + // these users have a read-only view of Foo + case 'read': + case 'dual_privileges_read': + case 'foo_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: true, + }); + break; + // these users have no access to even get the ui capabilities + case 'legacy_all': + case 'no_kibana_privileges': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + // all other users can't do anything with Foo + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/index.ts b/x-pack/test/ui_capabilities/security_only/tests/index.ts new file mode 100644 index 00000000000000..e69cef8b313603 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/index.ts @@ -0,0 +1,60 @@ +/* + * 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 { SecurityService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { isCustomRoleSpecification } from '../../common/types'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitesTests({ + loadTestFile, + getService, +}: KibanaFunctionalTestDefaultProviders) { + const securityService: SecurityService = getService('security'); + + describe('ui capabilities', function() { + this.tags('ciGroup5'); + + before(async () => { + for (const user of UserScenarios) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + + await securityService.user.create(user.username, { + password: user.password, + full_name: user.fullName, + roles: roles.map(role => role.name), + }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + }); + } + } + } + }); + + after(async () => { + for (const user of UserScenarios) { + await securityService.user.delete(user.username); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } + } + } + }); + + loadTestFile(require.resolve('./catalogue')); + loadTestFile(require.resolve('./foo')); + loadTestFile(require.resolve('./nav_links')); + loadTestFile(require.resolve('./saved_objects_management')); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts new file mode 100644 index 00000000000000..4b8cc06bd56887 --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { NavLinksBuilder } from '../../common/nav_links_builder'; +import { FeaturesService } from '../../common/services'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function navLinksTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const featuresService: FeaturesService = getService('features'); + + describe('navLinks', () => { + let navLinksBuilder: NavLinksBuilder; + before(async () => { + const features = await featuresService.get(); + navLinksBuilder = new NavLinksBuilder(features); + }); + + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + case 'superuser': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; + case 'all': + case 'read': + case 'dual_privileges_all': + case 'dual_privileges_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); + break; + case 'foo_all': + case 'foo_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.only('management', 'foo') + ); + break; + case 'legacy_all': + case 'no_kibana_privileges': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts new file mode 100644 index 00000000000000..403c1b6b904ddb --- /dev/null +++ b/x-pack/test/ui_capabilities/security_only/tests/saved_objects_management.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder'; +import { + GetUICapabilitiesFailureReason, + UICapabilitiesService, +} from '../../common/services/ui_capabilities'; +import { UserScenarios } from '../scenarios'; + +const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(false); + +// eslint-disable-next-line import/no-default-export +export default function savedObjectsManagementTests({ + getService, +}: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('savedObjectsManagement', () => { + UserScenarios.forEach(scenario => { + it(`${scenario.fullName}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get({ + username: scenario.username, + password: scenario.password, + }); + switch (scenario.username) { + case 'superuser': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () => + savedObjectsManagementBuilder.uiCapabilities('all') + ); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql(expected); + break; + case 'all': + case 'dual_privileges_all': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + all: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'read': + case 'dual_privileges_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + read: [ + 'config', + 'graph-workspace', + 'map', + 'canvas-workpad', + 'index-pattern', + 'visualization', + 'search', + 'dashboard', + 'timelion-sheet', + 'url', + 'infrastructure-ui-source', + ], + }) + ); + break; + case 'foo_all': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + all: ['foo'], + read: ['index-pattern', 'config'], + }) + ); + break; + case 'foo_read': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql( + savedObjectsManagementBuilder.build({ + read: ['foo', 'index-pattern', 'config'], + }) + ); + break; + case 'no_kibana_privileges': + case 'legacy_all': + expect(uiCapabilities.success).to.be(false); + expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/config.ts b/x-pack/test/ui_capabilities/spaces_only/config.ts new file mode 100644 index 00000000000000..12cfa1c282c8dd --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/config.ts @@ -0,0 +1,10 @@ +/* + * 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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { license: 'basic' }); diff --git a/x-pack/test/ui_capabilities/spaces_only/scenarios.ts b/x-pack/test/ui_capabilities/spaces_only/scenarios.ts new file mode 100644 index 00000000000000..c5f31c1ca22c0b --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/scenarios.ts @@ -0,0 +1,44 @@ +/* + * 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 { Space } from '../common/types'; + +// For all scenarios, we define both an instance in addition +// to a "type" definition so that we can use the exhaustive switch in +// typescript to ensure all scenarios are handled. + +interface EverythingSpace extends Space { + id: 'everything_space'; +} +const EverythingSpace: EverythingSpace = { + id: 'everything_space', + name: 'everything_space', + disabledFeatures: [], +}; + +interface NothingSpace extends Space { + id: 'nothing_space'; +} +const NothingSpace: NothingSpace = { + id: 'nothing_space', + name: 'nothing_space', + disabledFeatures: '*', +}; + +interface FooDisabledSpace extends Space { + id: 'foo_disabled_space'; +} +const FooDisabledSpace: FooDisabledSpace = { + id: 'foo_disabled_space', + name: 'foo_disabled_space', + disabledFeatures: ['foo'], +}; + +export const SpaceScenarios: [EverythingSpace, NothingSpace, FooDisabledSpace] = [ + EverythingSpace, + NothingSpace, + FooDisabledSpace, +]; diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts new file mode 100644 index 00000000000000..f0aac4183a0870 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function catalogueTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('catalogue', () => { + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + switch (scenario.id) { + case 'everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'nothing_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is disabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + case 'foo_disabled_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // only foo is disabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (value, catalogueId) => catalogueId !== 'foo' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/foo.ts b/x-pack/test/ui_capabilities/spaces_only/tests/foo.ts new file mode 100644 index 00000000000000..7840dc719cdc03 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/foo.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function fooTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('foo', () => { + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + switch (scenario.id) { + case 'everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: true, + edit: true, + delete: true, + show: true, + }); + break; + case 'nothing_space': + case 'foo_disabled_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('foo'); + expect(uiCapabilities.value!.foo).to.eql({ + create: false, + edit: false, + delete: false, + show: false, + }); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/index.ts b/x-pack/test/ui_capabilities/spaces_only/tests/index.ts new file mode 100644 index 00000000000000..0d8164aa584dd0 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/index.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. + */ + +import { SpacesService } from '../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { FeaturesService } from '../../common/services'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function uiCapabilitesTests({ + loadTestFile, + getService, +}: KibanaFunctionalTestDefaultProviders) { + const spacesService: SpacesService = getService('spaces'); + const featuresService: FeaturesService = getService('features'); + + describe('ui capabilities', function() { + this.tags('ciGroup5'); + + before(async () => { + const features = await featuresService.get(); + for (const space of SpaceScenarios) { + const disabledFeatures = + space.disabledFeatures === '*' ? Object.keys(features) : space.disabledFeatures; + await spacesService.create({ + ...space, + disabledFeatures, + }); + } + }); + + after(async () => { + for (const space of SpaceScenarios) { + await spacesService.delete(space.id); + } + }); + + loadTestFile(require.resolve('./catalogue')); + loadTestFile(require.resolve('./foo')); + loadTestFile(require.resolve('./nav_links')); + loadTestFile(require.resolve('./saved_objects_management')); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts new file mode 100644 index 00000000000000..03fd1934abba43 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/nav_links.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { NavLinksBuilder } from '../../common/nav_links_builder'; +import { FeaturesService } from '../../common/services'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function navLinksTests({ getService }: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const featuresService: FeaturesService = getService('features'); + + describe('navLinks', () => { + let navLinksBuilder: NavLinksBuilder; + before(async () => { + const features = await featuresService.get(); + navLinksBuilder = new NavLinksBuilder(features); + }); + + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + switch (scenario.id) { + case 'everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; + case 'nothing_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.only('management')); + break; + case 'foo_disabled_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.except('foo')); + break; + default: + throw new UnreachableError(scenario); + } + }); + }); + }); +} diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts b/x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts new file mode 100644 index 00000000000000..ef4f7b84003122 --- /dev/null +++ b/x-pack/test/ui_capabilities/spaces_only/tests/saved_objects_management.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { mapValues } from 'lodash'; +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; +import { SavedObjectsManagementBuilder } from '../../common/saved_objects_management_builder'; +import { UICapabilitiesService } from '../../common/services/ui_capabilities'; +import { SpaceScenarios } from '../scenarios'; + +const savedObjectsManagementBuilder = new SavedObjectsManagementBuilder(true); + +// eslint-disable-next-line import/no-default-export +export default function savedObjectsManagementTests({ + getService, +}: KibanaFunctionalTestDefaultProviders) { + const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + + describe('savedObjectsManagement', () => { + SpaceScenarios.forEach(scenario => { + it(`${scenario.name}`, async () => { + // spaces don't affect saved objects management, so we assert the same thing for every scenario + const uiCapabilities = await uiCapabilitiesService.get(null, scenario.id); + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('savedObjectsManagement'); + const expected = mapValues(uiCapabilities.value!.savedObjectsManagement, () => + savedObjectsManagementBuilder.uiCapabilities('all') + ); + expect(uiCapabilities.value!.savedObjectsManagement).to.eql(expected); + }); + }); + }); +}