From 6e1e8359e411db815f8863c5d586c272c0f5dec9 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 21 Apr 2021 22:53:04 -0700 Subject: [PATCH] [7.x] [Reporting] Kibana Application Privileges for Reporting (#94966) (#97777) * [Reporting] Kibana Application Privileges for Reporting (#94966) * Implement Reporting features as subfeatures of applications * add setting to the docker list * update doc images * finish docs * Apply suggestions from code review Co-authored-by: Kaarina Tungseth * Apply suggestions from code review Co-authored-by: Kaarina Tungseth * Apply suggestions from code review Co-authored-by: Kaarina Tungseth * typo fix * "PDF / PNG Reports" => "Reporting" * Update x-pack/plugins/reporting/server/config/index.ts Co-authored-by: Larry Gregory * Update x-pack/test/functional/apps/security/secure_roles_perm.js Co-authored-by: Larry Gregory * update ids of report privileges * combine dashboard privileges into 1 group * update jest snapshot * fix tests * fix tests * updates from feedback * add note * update screenshot * fix grammer * fix bad link breaks in doc * update doc heading * Apply suggestions documentation feedback Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * simplify * use const assertions * Apply text change suggestion from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * more test for oss_features and reporting subFeatures * reduce loc diff * fix snapshot * fix flakiness in licensing plugin public functional tests Co-authored-by: Kaarina Tungseth Co-authored-by: Larry Gregory Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> # Conflicts: # x-pack/plugins/reporting/server/core.ts # x-pack/plugins/reporting/server/lib/enqueue_job.test.ts # x-pack/plugins/reporting/server/lib/store/store.test.ts # x-pack/plugins/reporting/server/lib/tasks/execute_report.test.ts # x-pack/plugins/reporting/server/lib/tasks/monitor_report.test.ts # x-pack/plugins/reporting/server/plugin.ts # x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts * fix ci * fix eslint * skip flaky suite (#53575) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Tiago Costa --- docs/settings/reporting-settings.asciidoc | 15 +- docs/user/reporting/index.asciidoc | 13 +- .../security/images/reporting-custom-role.png | Bin 0 -> 151494 bytes docs/user/security/reporting.asciidoc | 143 ++++-- .../resources/base/bin/kibana-docker | 1 + x-pack/plugins/canvas/kibana.json | 1 + .../__stories__/share_menu.stories.tsx | 1 + .../share_menu/share_menu.component.tsx | 34 +- .../workpad_header/share_menu/share_menu.ts | 1 + x-pack/plugins/canvas/public/plugin.tsx | 2 + .../canvas/public/services/context.tsx | 1 + .../plugins/canvas/public/services/index.ts | 4 + .../canvas/public/services/reporting.ts | 35 ++ .../canvas/public/services/stubs/index.ts | 2 + .../canvas/public/services/stubs/reporting.ts | 12 + x-pack/plugins/canvas/server/feature.test.ts | 193 ++++++++ x-pack/plugins/canvas/server/feature.ts | 81 ++++ x-pack/plugins/canvas/server/plugin.ts | 33 +- .../__snapshots__/oss_features.test.ts.snap | 456 ++++++++++++++++++ x-pack/plugins/features/server/mocks.ts | 1 + .../features/server/oss_features.test.ts | 40 +- .../plugins/features/server/oss_features.ts | 113 ++++- x-pack/plugins/features/server/plugin.ts | 18 + x-pack/plugins/reporting/public/index.ts | 1 + .../get_csv_panel_action.test.ts | 134 ++++- .../panel_actions/get_csv_panel_action.tsx | 34 +- x-pack/plugins/reporting/public/plugin.ts | 71 +-- .../register_csv_reporting.tsx | 54 ++- .../register_pdf_png_reporting.tsx | 284 ++++++----- .../server/config/create_config.test.ts | 3 + .../reporting/server/config/index.test.ts | 16 +- .../plugins/reporting/server/config/index.ts | 13 +- .../reporting/server/config/schema.test.ts | 2 + .../plugins/reporting/server/config/schema.ts | 1 + x-pack/plugins/reporting/server/core.ts | 70 ++- .../export_types/csv/execute_job.test.ts | 4 +- .../csv_searchsource/execute_job.test.ts | 9 +- .../png/execute_job/index.test.ts | 38 +- .../printable_pdf/execute_job/index.test.ts | 13 +- .../printable_pdf/lib/get_custom_logo.test.ts | 11 +- x-pack/plugins/reporting/server/index.ts | 15 +- .../server/lib/create_worker.test.ts | 10 +- .../reporting/server/lib/enqueue_job.test.ts | 91 ++++ .../reporting/server/lib/store/store.test.ts | 12 +- .../plugins/reporting/server/plugin.test.ts | 17 - x-pack/plugins/reporting/server/plugin.ts | 37 +- .../routes/csv_searchsource_immediate.ts | 10 + .../server/routes/diagnostic/browser.test.ts | 21 +- .../server/routes/diagnostic/config.test.ts | 31 +- .../routes/diagnostic/screenshot.test.ts | 12 +- .../server/routes/generate_from_jobparams.ts | 26 +- .../server/routes/generation.test.ts | 34 +- .../reporting/server/routes/jobs.test.ts | 76 +-- .../lib/authorized_user_pre_routing.test.ts | 108 +++-- .../routes/lib/authorized_user_pre_routing.ts | 45 +- .../server/routes/lib/job_response_handler.ts | 63 +-- .../create_mock_reportingplugin.ts | 17 +- x-pack/plugins/reporting/server/types.ts | 10 +- .../usage/reporting_usage_collector.test.ts | 15 +- x-pack/scripts/functional_tests.js | 1 + .../apis/security/license_downgrade.ts | 1 + .../apis/security/privileges.ts | 14 +- .../feature_controls/canvas_security.ts | 2 +- x-pack/test/functional/apps/canvas/reports.ts | 12 +- .../apps/dashboard/reporting/screenshots.ts | 26 +- .../feature_controls/discover_security.ts | 21 +- .../functional/apps/lens/lens_reporting.ts | 12 +- .../feature_controls/management_security.ts | 2 +- .../reporting_management/report_listing.ts | 12 +- .../saved_objects_management_security.ts | 2 +- .../apps/security/secure_roles_perm.js | 16 +- .../feature_controls/visualize_security.ts | 2 +- x-pack/test/functional/config.js | 5 +- .../test/licensing_plugin/public/updates.ts | 11 +- .../reporting_and_security/index.ts | 1 + .../services/scenarios.ts | 32 +- ...eporting_and_deprecated_security.config.ts | 26 + .../index.ts | 58 +++ .../management.ts | 37 ++ .../security_roles_privileges.ts | 109 +++++ .../reporting_and_security/index.ts | 43 +- .../reporting_and_security/management.ts | 7 +- .../security_roles_privileges.ts | 44 +- .../tests/anonymous/capabilities.ts | 36 ++ 84 files changed, 2416 insertions(+), 724 deletions(-) create mode 100644 docs/user/security/images/reporting-custom-role.png create mode 100644 x-pack/plugins/canvas/public/services/reporting.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/reporting.ts create mode 100644 x-pack/plugins/canvas/server/feature.test.ts create mode 100644 x-pack/plugins/canvas/server/feature.ts create mode 100644 x-pack/plugins/reporting/server/lib/enqueue_job.test.ts create mode 100644 x-pack/test/reporting_functional/reporting_and_deprecated_security.config.ts create mode 100644 x-pack/test/reporting_functional/reporting_and_deprecated_security/index.ts create mode 100644 x-pack/test/reporting_functional/reporting_and_deprecated_security/management.ts create mode 100644 x-pack/test/reporting_functional/reporting_and_deprecated_security/security_roles_privileges.ts diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9bb11f3f99a15..084ac633e9bca 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -275,9 +275,20 @@ For information about {kib} memory limits, see <> setting. Defaults to `.reporting`. +| [[xpack-reporting-roles-enabled]] `xpack.reporting.roles.enabled` + | deprecated:[7.13.0,This setting must be set to `false` in 8.0.] When `true`, grants users + access to the {report-features} by assigning reporting roles, specified by `xpack.reporting.roles.allow`. + Granting access to users this way is deprecated. Set to `false` and use + {kibana-ref}/kibana-privileges.html[{kib} privileges] instead. + Defaults to `true`. + | `xpack.reporting.roles.allow` - | Specifies the roles in addition to superusers that can use reporting. - Defaults to `[ "reporting_user" ]`. + + | deprecated:[7.13.0,This setting will be removed in 8.0.] Specifies the roles, + in addition to superusers, that can generate reports, using the {ref}/security-api.html#security-role-apis[{es} role management APIs]. + Requires `xpack.reporting.roles.enabled` to be `true`. + Granting access to users this way is deprecated. Use + {kibana-ref}/kibana-privileges.html[{kib} privileges] instead. + Defaults to `[ "reporting_user" ]`. |=== diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc index dbe433466c961..144ed1ea28c93 100644 --- a/docs/user/reporting/index.asciidoc +++ b/docs/user/reporting/index.asciidoc @@ -31,10 +31,15 @@ for different operating systems. [[reporting-required-privileges]] == Roles and privileges -To generate a report, you must have the `reporting_user` role. You also need -the appropriate {kib} privileges to access the objects that you -want to report on and the {es} indices. See <> -for an example. +When security is enabled, access to the {report-features} is controlled by security privileges. In versions 7.12 and earlier, you can grant access to the {report-features} +by assigning users the `reporting_user` role in {es}. In 7.13 and later, you can configure *Reporting* to use +<>. It is recommended that *Reporting* is configured to +use {kib} privileges by setting <> to `false`. By using {kib} privileges, you can define +custom roles that grant *Reporting* privileges as sub-features of {kib} applications in *Role Management*. + +Users must also have the {kib} privileges to access the saved objects and associated {es} indices included in the generated reports. +For an example, refer to <>. [float] [[manually-generate-reports]] diff --git a/docs/user/security/images/reporting-custom-role.png b/docs/user/security/images/reporting-custom-role.png new file mode 100644 index 0000000000000000000000000000000000000000..4034ca366580640b3dfdc9944dc41ec24a9d3da3 GIT binary patch literal 151494 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%0AVl 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 literal 0 HcmV?d00001 diff --git a/docs/user/security/reporting.asciidoc b/docs/user/security/reporting.asciidoc index e69643ef9712a..2f331e252c492 100644 --- a/docs/user/security/reporting.asciidoc +++ b/docs/user/security/reporting.asciidoc @@ -16,17 +16,30 @@ For more information, see //// [[reporting-app-users]] -To enable users to generate reports, you must assign them the built-in `reporting_user` -role. Users will also need the appropriate <> to access the objects -to report on and the {es} indices. +Access to reporting features is limited to privileged users. In older versions of Kibana, you could only grant +users the privilege by assigning them the `reporting_user` role in Elasticsearch. In 7.13 and above, you have +the option to create your own roles that grant access to reporting features using <>. + +It is recommended that you set `xpack.reporting.roles.enabled: false` in your kibana.yml to begin using Kibana +privileges. This will allow users to only see Reporting widgets in applications when they have privilege to use +them. + +[NOTE] +============================================================================ +The default value of `xpack.reporting.roles.enabled` is `true` for 7.x versions of Kibana. To migrate users to the +new method of securing access to *Reporting*, you must explicitly set `xpack.reporting.roles.enabled: false` in +`kibana.yml`. In the next major version of Kibana, having this set to `false` will be the only valid configuration. +============================================================================ + +This document discusses how to create a role that grants access to reporting features using the new method of +Kibana application privileges. [float] [[reporting-roles-management-ui]] -=== If you are using the `native` realm +=== Create the role in the `native` realm -To assign roles, use the *Roles* UI or <>. -This example shows how to use *Roles* page to create a user who has a custom role and the -`reporting_user` role. +To create roles, use the *Roles* UI or <>. This example shows how to +create a role that grants reporting feature privileges in {kib} applications. . Open the main menu, then click *Stack Management > Roles*. @@ -42,60 +55,69 @@ For more information, see {ref}/security-privileges.html[Security privileges]. [role="screenshot"] image::user/security/images/reporting-privileges-example.png["Reporting privileges"] -. Add space privileges. +. Add space privileges for the {kib} applications that allow access to the reporting options. ++ +To allow users to create CSV reports in *Discover*, or PDF reports in *Canvas*, +*Visualize Library*, and *Dashboard*, click *Add Kibana privilege* for each application, +then select the privileges to generate +reports. For example, select *All* privileges for all features, or *Customize* to grant +the privilege to generate reports for only specific applications. ++ +[role="screenshot"] +image::user/security/images/reporting-custom-role.png["Reporting custom role"] ++ +[NOTE] +============================================================================ +Granting users access to reporting features in any application also grants them access to manage their reports in *Stack Management > Reporting*. +============================================================================ + -Reporting users typically save searches, create -visualizations, and build dashboards. They require a space -that provides read and write privileges in -*Discover* and *Dashboard*. - . Save your new role. -. Open the main menu, then click *Stack Management > Users*, add a new user, and assign the user the built-in -`reporting_user` role and your new custom role, `custom_reporting_user`. - -[float] -==== With a custom index - -If you are using Reporting with a custom index, -the `xpack.reporting.index` setting should begin -with `.reporting-*`. The default {kib} system user has -`all` privileges against the `.reporting-*` pattern of indices. - -[source,js] -xpack.reporting.index: '.reporting-custom-index' - -If you use a different pattern for the `xpack.reporting.index` setting, -you must create a custom role with appropriate access to the index, similar -to the following: - -. Open the main menu, then click *Stack Management > Roles*. -. Click *Create role*, then name the role `custom-reporting-user`. -. Specify the custom index and assign it the `all` index privilege. -. Open the main menu, then click *Stack Management > Users* and create a new user with -the `kibana_system` role and the `custom-reporting-user` role. -. Configure {kib} to use the new account: -[source,js] -elasticsearch.username: 'custom_kibana_system' +. Open the main menu, then click *Stack Management > Users*, add a new user, and assign the user +your new `custom_reporting_user` role. [float] [[reporting-roles-user-api]] ==== With the user API -This example uses the {ref}/security-api-put-user.html[user API] to create a user who has the -`reporting_user` role and the `kibana_admin` role: +This example uses the {ref}/security-api-put-role.html[role API] to create a role that +grants the privilege to generate reports in *Canvas*, *Discover*, *Visualize Library*, and *Dashboard*. +This role is meant to be granted to users in combination with other roles that grant read access +to the data in {es}, and at least read access in the applications +where they'll generate reports. [source, sh] --------------------------------------------------------------- -POST /_security/user/reporter +POST /_security/role/custom_reporting_user { - "password" : "x-pack-test-password", - "roles" : ["kibana_admin", "reporting_user"], - "full_name" : "Reporting User" + metadata: {}, + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + base: [], + feature: { + dashboard: [ + 'generate_report', <1> + 'download_csv_report' <2> + ], + discover: ['generate_report'], <3> + canvas: ['generate_report'], <4> + visualize: ['generate_report'], <5> + }, + spaces: ['*'], + } + ] } --------------------------------------------------------------- +// CONSOLE + +<1> Grants access to generate PNG and PDF reports in *Dashboard*. +<2> Grants access to download CSV files from saved search panels in *Dashboard*. +<3> Grants access to generate CSV reports from saved searches in *Discover*. +<4> Grants access to generate PDF reports in *Canvas*. +<5> Grants access to generate PNG and PDF reports in *Visualize Library*. [float] -=== If you are using an external identity provider +=== When using an external provider If you are using an external identity provider, such as LDAP or Active Directory, you can either assign @@ -113,6 +135,35 @@ reporting_user: - "cn=Bill Murray,dc=example,dc=com" -------------------------------------------------------------------------------- +[float] +=== With a custom index + +If you are using a custom index, +the `xpack.reporting.index` setting should begin +with `.reporting-*`. The default {kib} system user has +`all` privileges against the `.reporting-*` pattern of indices. + +[source,js] +xpack.reporting.index: '.reporting-custom-index' + +If you use a different pattern for the `xpack.reporting.index` setting, +you must create a custom `kibana_system` user with appropriate access to the index, similar +to the following: + +. Open the main menu, then click *Stack Management > Roles*. +. Click *Create role*, then name the role `custom-reporting-user`. +. Specify the custom index and assign it the `all` index privilege. +. Open the main menu, then click *Stack Management > Users* and create a new user with +the `kibana_system` role and the `custom-reporting-user` role. +. Configure {kib} to use the new account: +[source,js] +elasticsearch.username: 'custom_kibana_system' + +[NOTE] +============================================================================ +Setting a custom index for *Reporting* is not supported in the next major version of Kibana. +============================================================================ + [role="xpack"] [[securing-reporting]] === Secure the reporting endpoints diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 877be1afeebe8..97e3ab3bb5b12 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -273,6 +273,7 @@ kibana_vars=( xpack.reporting.queue.pollIntervalErrorMultiplier xpack.reporting.queue.timeout xpack.reporting.roles.allow + xpack.reporting.roles.enabled xpack.rollup.enabled xpack.ruleRegistry.unsafe.write.enabled xpack.searchprofiler.enabled diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index cff1a3e7fa8b7..6213ecb58347c 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -18,6 +18,7 @@ ], "optionalPlugins": [ "home", + "reporting", "usageCollection" ], "requiredBundles": [ diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx index 6943195f03dad..bca96f3851e37 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__stories__/share_menu.stories.tsx @@ -12,6 +12,7 @@ import { ShareMenu } from '../share_menu.component'; storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => ( { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx index 2cd545f5d65b3..0d2e877bebdfd 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.component.tsx @@ -28,6 +28,8 @@ export type OnCloseFn = (type: CloseTypes) => void; export type GetExportUrlFn = (type: ExportUrlTypes, layout: LayoutType) => string; export interface Props { + /** Flag to include the Reporting option only if Reporting is enabled */ + includeReporting: boolean; /** Handler to invoke when an export URL is copied to the clipboard. */ onCopy: OnCopyFn; /** Handler to invoke when an end product is exported. */ @@ -39,7 +41,12 @@ export interface Props { /** * The Menu for Exporting a Workpad from Canvas. */ -export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExportUrl }) => { +export const ShareMenu: FunctionComponent = ({ + includeReporting, + onCopy, + onExport, + getExportUrl, +}) => { const [showFlyout, setShowFlyout] = useState(false); const onClose = () => { @@ -73,16 +80,18 @@ export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExpor closePopover(); }, }, - { - name: strings.getShareDownloadPDFTitle(), - icon: 'document', - panel: { - id: 1, - title: strings.getShareDownloadPDFTitle(), - content: getPDFPanel(closePopover), - }, - 'data-test-subj': 'sharePanel-PDFReports', - }, + includeReporting + ? { + name: strings.getShareDownloadPDFTitle(), + icon: 'document', + panel: { + id: 1, + title: strings.getShareDownloadPDFTitle(), + content: getPDFPanel(closePopover), + }, + 'data-test-subj': 'sharePanel-PDFReports', + } + : false, { name: strings.getShareWebsiteTitle(), icon: , @@ -91,7 +100,7 @@ export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExpor closePopover(); }, }, - ], + ].filter(Boolean), }); const shareControl = (togglePopover: React.MouseEventHandler) => ( @@ -123,6 +132,7 @@ export const ShareMenu: FunctionComponent = ({ onCopy, onExport, getExpor }; ShareMenu.propTypes = { + includeReporting: PropTypes.bool.isRequired, onCopy: PropTypes.func.isRequired, onExport: PropTypes.func.isRequired, getExportUrl: PropTypes.func.isRequired, diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts index a0448504db54b..47b5e755d439c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/share_menu.ts @@ -46,6 +46,7 @@ export const ShareMenu = compose( withServices, withProps( ({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({ + includeReporting: services.reporting.includeReporting(), getExportUrl: (type, layout) => { if (type === 'pdf') { const pdfUrl = getPdfUrl( diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 486cd03eb9dd6..750b542116a75 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -7,6 +7,7 @@ import { BehaviorSubject } from 'rxjs'; import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public'; +import { ReportingStart } from '../../reporting/public'; import { CoreSetup, CoreStart, @@ -49,6 +50,7 @@ export interface CanvasSetupDeps { export interface CanvasStartDeps { embeddable: EmbeddableStart; expressions: ExpressionsStart; + reporting?: ReportingStart; inspector: InspectorStart; uiActions: UiActionsStart; charts: ChartsPluginStart; diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 3865d98caf2b3..4c18aa68fb51e 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -54,6 +54,7 @@ export const ServicesProvider: FC<{ notify: specifiedProviders.notify.getService(), platform: specifiedProviders.platform.getService(), navLink: specifiedProviders.navLink.getService(), + reporting: specifiedProviders.reporting.getService(), labs: specifiedProviders.labs.getService(), }; return {children}; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index 9bfc41a782edc..1566d6f28085a 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -14,6 +14,7 @@ import { navLinkServiceFactory } from './nav_link'; import { embeddablesServiceFactory } from './embeddables'; import { expressionsServiceFactory } from './expressions'; import { labsServiceFactory } from './labs'; +import { reportingServiceFactory } from './reporting'; export { NotifyService } from './notify'; export { PlatformService } from './platform'; @@ -79,6 +80,7 @@ export const services = { notify: new CanvasServiceProvider(notifyServiceFactory), platform: new CanvasServiceProvider(platformServiceFactory), navLink: new CanvasServiceProvider(navLinkServiceFactory), + reporting: new CanvasServiceProvider(reportingServiceFactory), labs: new CanvasServiceProvider(labsServiceFactory), }; @@ -90,6 +92,7 @@ export interface CanvasServices { notify: ServiceFromProvider; platform: ServiceFromProvider; navLink: ServiceFromProvider; + reporting: ServiceFromProvider; labs: ServiceFromProvider; } @@ -117,4 +120,5 @@ export const { platform: platformService, navLink: navLinkService, expressions: expressionsService, + reporting: reportingService, } = services; diff --git a/x-pack/plugins/canvas/public/services/reporting.ts b/x-pack/plugins/canvas/public/services/reporting.ts new file mode 100644 index 0000000000000..3299363cd5c7f --- /dev/null +++ b/x-pack/plugins/canvas/public/services/reporting.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CanvasServiceFactory } from './'; + +export interface ReportingService { + includeReporting: () => boolean; +} + +export const reportingServiceFactory: CanvasServiceFactory = ( + _coreSetup, + coreStart, + _setupPlugins, + startPlugins +): ReportingService => { + const { reporting } = startPlugins; + if (!reporting) { + // Reporting is not enabled + return { includeReporting: () => false }; + } + + if (reporting.usesUiCapabilities()) { + // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability + return { + includeReporting: () => coreStart.application.capabilities.canvas?.generatePdf === true, + }; + } + + // Reporting is enabled as an Elasticsearch feature (Legacy/Deprecated) + return { includeReporting: () => true }; +}; diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 91bda2556284e..786582ed94bd2 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -8,6 +8,7 @@ import { CanvasServices, services } from '../'; import { embeddablesService } from './embeddables'; import { expressionsService } from './expressions'; +import { reportingService } from './reporting'; import { navLinkService } from './nav_link'; import { notifyService } from './notify'; import { labsService } from './labs'; @@ -16,6 +17,7 @@ import { platformService } from './platform'; export const stubs: CanvasServices = { embeddables: embeddablesService, expressions: expressionsService, + reporting: reportingService, navLink: navLinkService, notify: notifyService, platform: platformService, diff --git a/x-pack/plugins/canvas/public/services/stubs/reporting.ts b/x-pack/plugins/canvas/public/services/stubs/reporting.ts new file mode 100644 index 0000000000000..f257dd14543ec --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/reporting.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReportingService } from '../reporting'; + +export const reportingService: ReportingService = { + includeReporting: () => true, +}; diff --git a/x-pack/plugins/canvas/server/feature.test.ts b/x-pack/plugins/canvas/server/feature.test.ts new file mode 100644 index 0000000000000..cd5f0a4b4dc01 --- /dev/null +++ b/x-pack/plugins/canvas/server/feature.test.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReportingStart } from '../../reporting/server/types'; +import { getCanvasFeature } from './feature'; + +let mockReportingPlugin: ReportingStart; +beforeEach(() => { + mockReportingPlugin = { + usesUiCapabilities: () => false, + }; +}); + +it('Provides a feature declaration ', () => { + expect(getCanvasFeature({ reporting: mockReportingPlugin })).toMatchInlineSnapshot(` + Object { + "app": Array [ + "canvas", + "kibana", + ], + "catalogue": Array [ + "canvas", + ], + "category": Object { + "euiIconType": "logoKibana", + "id": "kibana", + "label": "Analytics", + "order": 1000, + }, + "id": "canvas", + "management": Object {}, + "name": "Canvas", + "order": 300, + "privileges": Object { + "all": Object { + "app": Array [ + "canvas", + "kibana", + ], + "catalogue": Array [ + "canvas", + ], + "savedObject": Object { + "all": Array [ + "canvas-workpad", + "canvas-element", + ], + "read": Array [ + "index-pattern", + ], + }, + "ui": Array [ + "save", + "show", + ], + }, + "read": Object { + "app": Array [ + "canvas", + "kibana", + ], + "catalogue": Array [ + "canvas", + ], + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + "canvas-workpad", + "canvas-element", + ], + }, + "ui": Array [ + "show", + ], + }, + }, + "subFeatures": Array [], + } + `); +}); + +it(`Calls on Reporting whether to include Generate PDF as a sub-feature`, () => { + mockReportingPlugin = { + usesUiCapabilities: () => true, + }; + expect(getCanvasFeature({ reporting: mockReportingPlugin })).toMatchInlineSnapshot(` + Object { + "app": Array [ + "canvas", + "kibana", + ], + "catalogue": Array [ + "canvas", + ], + "category": Object { + "euiIconType": "logoKibana", + "id": "kibana", + "label": "Analytics", + "order": 1000, + }, + "id": "canvas", + "management": Object { + "insightsAndAlerting": Array [ + "reporting", + ], + }, + "name": "Canvas", + "order": 300, + "privileges": Object { + "all": Object { + "app": Array [ + "canvas", + "kibana", + ], + "catalogue": Array [ + "canvas", + ], + "savedObject": Object { + "all": Array [ + "canvas-workpad", + "canvas-element", + ], + "read": Array [ + "index-pattern", + ], + }, + "ui": Array [ + "save", + "show", + ], + }, + "read": Object { + "app": Array [ + "canvas", + "kibana", + ], + "catalogue": Array [ + "canvas", + ], + "savedObject": Object { + "all": Array [], + "read": Array [ + "index-pattern", + "canvas-workpad", + "canvas-element", + ], + }, + "ui": Array [ + "show", + ], + }, + }, + "subFeatures": Array [ + Object { + "name": "Reporting", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "generateReport", + ], + "id": "generate_report", + "includeIn": "all", + "management": Object { + "insightsAndAlerting": Array [ + "reporting", + ], + }, + "minimumLicense": "platinum", + "name": "Generate PDF reports", + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "generatePdf", + ], + }, + ], + }, + ], + }, + ], + } + `); +}); diff --git a/x-pack/plugins/canvas/server/feature.ts b/x-pack/plugins/canvas/server/feature.ts new file mode 100644 index 0000000000000..33368a8020b1e --- /dev/null +++ b/x-pack/plugins/canvas/server/feature.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { KibanaFeatureConfig } from '../../features/common'; +import { ReportingSetup } from '../../reporting/server'; + +/* + * Register Canvas as a Kibana feature, + * with Reporting sub-feature integration (if enabled) + */ +export function getCanvasFeature(plugins: { reporting?: ReportingSetup }): KibanaFeatureConfig { + const includeReporting = plugins.reporting && plugins.reporting.usesUiCapabilities(); + + return { + id: 'canvas', + name: 'Canvas', + order: 300, + category: DEFAULT_APP_CATEGORIES.kibana, + app: ['canvas', 'kibana'], + management: { + ...(includeReporting ? { insightsAndAlerting: ['reporting'] } : {}), + }, + catalogue: ['canvas'], + privileges: { + all: { + app: ['canvas', 'kibana'], + catalogue: ['canvas'], + savedObject: { + all: ['canvas-workpad', 'canvas-element'], + read: ['index-pattern'], + }, + ui: ['save', 'show'], + }, + read: { + app: ['canvas', 'kibana'], + catalogue: ['canvas'], + savedObject: { + all: [], + read: ['index-pattern', 'canvas-workpad', 'canvas-element'], + }, + ui: ['show'], + }, + }, + subFeatures: [ + ...(includeReporting + ? ([ + { + name: i18n.translate('xpack.canvas.features.reporting.pdfFeatureName', { + defaultMessage: 'Reporting', + }), + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'generate_report', + name: i18n.translate('xpack.canvas.features.reporting.pdf', { + defaultMessage: 'Generate PDF reports', + }), + includeIn: 'all', + management: { insightsAndAlerting: ['reporting'] }, + minimumLicense: 'platinum', + savedObject: { all: [], read: [] }, + api: ['generateReport'], + ui: ['generatePdf'], + }, + ], + }, + ], + }, + ] as const) + : []), + ], + }; +} diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 345f6099009fc..c95d825fb9b0b 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -10,8 +10,9 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import { ReportingSetup } from '../../reporting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { getCanvasFeature } from './feature'; import { initRoutes } from './routes'; import { registerCanvasUsageCollector } from './collectors'; import { loadSampleData } from './sample_data'; @@ -24,6 +25,7 @@ interface PluginsSetup { features: FeaturesPluginSetup; home: HomeServerPluginSetup; bfetch: BfetchServerSetup; + reporting?: ReportingSetup; usageCollection?: UsageCollectionSetup; } @@ -38,34 +40,7 @@ export class CanvasPlugin implements Plugin { coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); - plugins.features.registerKibanaFeature({ - id: 'canvas', - name: 'Canvas', - order: 300, - category: DEFAULT_APP_CATEGORIES.kibana, - app: ['canvas', 'kibana'], - catalogue: ['canvas'], - privileges: { - all: { - app: ['canvas', 'kibana'], - catalogue: ['canvas'], - savedObject: { - all: ['canvas-workpad', 'canvas-element'], - read: ['index-pattern'], - }, - ui: ['save', 'show'], - }, - read: { - app: ['canvas', 'kibana'], - catalogue: ['canvas'], - savedObject: { - all: [], - read: ['index-pattern', 'canvas-workpad', 'canvas-element'], - }, - ui: ['show'], - }, - }, - }); + plugins.features.registerKibanaFeature(getCanvasFeature(plugins)); const canvasRouter = coreSetup.http.createRouter(); diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 5c259b4c7b72e..88712f2ac14c0 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -1,5 +1,461 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`buildOSSFeatures returns features excluding reporting subfeatures 1`] = ` +Array [ + Object { + "id": "discover", + "subFeatures": Array [ + Object { + "name": "Short URLs", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "id": "url_create", + "includeIn": "all", + "name": "Create Short URLs", + "savedObject": Object { + "all": Array [ + "url", + ], + "read": Array [], + }, + "ui": Array [ + "createShortUrl", + ], + }, + ], + }, + ], + }, + Object { + "name": "Store Search Sessions", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "store_search_session", + ], + "id": "store_search_session", + "includeIn": "all", + "management": Object { + "kibana": Array [ + "search_sessions", + ], + }, + "name": "Store Search Sessions", + "savedObject": Object { + "all": Array [ + "search-session", + ], + "read": Array [], + }, + "ui": Array [ + "storeSearchSession", + ], + }, + ], + }, + ], + }, + ], + }, + Object { + "id": "visualize", + "subFeatures": Array [ + Object { + "name": "Short URLs", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "id": "url_create", + "includeIn": "all", + "name": "Create Short URLs", + "savedObject": Object { + "all": Array [ + "url", + ], + "read": Array [], + }, + "ui": Array [ + "createShortUrl", + ], + }, + ], + }, + ], + }, + ], + }, + Object { + "id": "dashboard", + "subFeatures": Array [ + Object { + "name": "Short URLs", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "id": "url_create", + "includeIn": "all", + "name": "Create Short URLs", + "savedObject": Object { + "all": Array [ + "url", + ], + "read": Array [], + }, + "ui": Array [ + "createShortUrl", + ], + }, + ], + }, + ], + }, + Object { + "name": "Store Search Sessions", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "store_search_session", + ], + "id": "store_search_session", + "includeIn": "all", + "management": Object { + "kibana": Array [ + "search_sessions", + ], + }, + "name": "Store Search Sessions", + "savedObject": Object { + "all": Array [ + "search-session", + ], + "read": Array [], + }, + "ui": Array [ + "storeSearchSession", + ], + }, + ], + }, + ], + }, + ], + }, + Object { + "id": "dev_tools", + "subFeatures": undefined, + }, + Object { + "id": "advancedSettings", + "subFeatures": undefined, + }, + Object { + "id": "indexPatterns", + "subFeatures": undefined, + }, + Object { + "id": "savedObjectsManagement", + "subFeatures": undefined, + }, +] +`; + +exports[`buildOSSFeatures returns features including reporting subfeatures 1`] = ` +Array [ + Object { + "id": "discover", + "subFeatures": Array [ + Object { + "name": "Short URLs", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "id": "url_create", + "includeIn": "all", + "name": "Create Short URLs", + "savedObject": Object { + "all": Array [ + "url", + ], + "read": Array [], + }, + "ui": Array [ + "createShortUrl", + ], + }, + ], + }, + ], + }, + Object { + "name": "Store Search Sessions", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "store_search_session", + ], + "id": "store_search_session", + "includeIn": "all", + "management": Object { + "kibana": Array [ + "search_sessions", + ], + }, + "name": "Store Search Sessions", + "savedObject": Object { + "all": Array [ + "search-session", + ], + "read": Array [], + }, + "ui": Array [ + "storeSearchSession", + ], + }, + ], + }, + ], + }, + Object { + "name": "Reporting", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "generateReport", + ], + "id": "generate_report", + "includeIn": "all", + "management": Object { + "insightsAndAlerting": Array [ + "reporting", + ], + }, + "name": "Generate CSV reports", + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "generateCsv", + ], + }, + ], + }, + ], + }, + ], + }, + Object { + "id": "visualize", + "subFeatures": Array [ + Object { + "name": "Short URLs", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "id": "url_create", + "includeIn": "all", + "name": "Create Short URLs", + "savedObject": Object { + "all": Array [ + "url", + ], + "read": Array [], + }, + "ui": Array [ + "createShortUrl", + ], + }, + ], + }, + ], + }, + Object { + "name": "Reporting", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "generateReport", + ], + "id": "generate_report", + "includeIn": "all", + "management": Object { + "insightsAndAlerting": Array [ + "reporting", + ], + }, + "minimumLicense": "platinum", + "name": "Generate PDF or PNG reports", + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "generateScreenshot", + ], + }, + ], + }, + ], + }, + ], + }, + Object { + "id": "dashboard", + "subFeatures": Array [ + Object { + "name": "Short URLs", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "id": "url_create", + "includeIn": "all", + "name": "Create Short URLs", + "savedObject": Object { + "all": Array [ + "url", + ], + "read": Array [], + }, + "ui": Array [ + "createShortUrl", + ], + }, + ], + }, + ], + }, + Object { + "name": "Store Search Sessions", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "store_search_session", + ], + "id": "store_search_session", + "includeIn": "all", + "management": Object { + "kibana": Array [ + "search_sessions", + ], + }, + "name": "Store Search Sessions", + "savedObject": Object { + "all": Array [ + "search-session", + ], + "read": Array [], + }, + "ui": Array [ + "storeSearchSession", + ], + }, + ], + }, + ], + }, + Object { + "name": "Reporting", + "privilegeGroups": Array [ + Object { + "groupType": "independent", + "privileges": Array [ + Object { + "api": Array [ + "generateReport", + ], + "id": "generate_report", + "includeIn": "all", + "management": Object { + "insightsAndAlerting": Array [ + "reporting", + ], + }, + "minimumLicense": "platinum", + "name": "Generate PDF or PNG reports", + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "generateScreenshot", + ], + }, + Object { + "api": Array [ + "downloadCsv", + ], + "id": "download_csv_report", + "includeIn": "all", + "management": Object { + "insightsAndAlerting": Array [ + "reporting", + ], + }, + "name": "Download CSV reports from Saved Search panels", + "savedObject": Object { + "all": Array [], + "read": Array [], + }, + "ui": Array [ + "downloadCsv", + ], + }, + ], + }, + ], + }, + ], + }, + Object { + "id": "dev_tools", + "subFeatures": undefined, + }, + Object { + "id": "advancedSettings", + "subFeatures": undefined, + }, + Object { + "id": "indexPatterns", + "subFeatures": undefined, + }, + Object { + "id": "savedObjectsManagement", + "subFeatures": undefined, + }, +] +`; + exports[`buildOSSFeatures with a basic license returns the advancedSettings feature augmented with appropriate sub feature privileges 1`] = ` Array [ Object { diff --git a/x-pack/plugins/features/server/mocks.ts b/x-pack/plugins/features/server/mocks.ts index aa92694050766..7b10a185dd0db 100644 --- a/x-pack/plugins/features/server/mocks.ts +++ b/x-pack/plugins/features/server/mocks.ts @@ -14,6 +14,7 @@ const createSetup = (): jest.Mocked => { getFeaturesUICapabilities: jest.fn(), registerKibanaFeature: jest.fn(), registerElasticsearchFeature: jest.fn(), + enableReportingUiCapabilities: jest.fn(), }; }; diff --git a/x-pack/plugins/features/server/oss_features.test.ts b/x-pack/plugins/features/server/oss_features.test.ts index b86fa726b3050..86705cae6d5a6 100644 --- a/x-pack/plugins/features/server/oss_features.test.ts +++ b/x-pack/plugins/features/server/oss_features.test.ts @@ -14,7 +14,11 @@ import { LicenseType } from '../../licensing/server'; describe('buildOSSFeatures', () => { it('returns features including timelion', () => { expect( - buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: true }).map((f) => f.id) + buildOSSFeatures({ + savedObjectTypes: ['foo', 'bar'], + includeTimelion: true, + includeReporting: false, + }).map((f) => f.id) ).toMatchInlineSnapshot(` Array [ "discover", @@ -31,9 +35,11 @@ Array [ it('returns features excluding timelion', () => { expect( - buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: false }).map( - (f) => f.id - ) + buildOSSFeatures({ + savedObjectTypes: ['foo', 'bar'], + includeTimelion: false, + includeReporting: false, + }).map((f) => f.id) ).toMatchInlineSnapshot(` Array [ "discover", @@ -47,7 +53,31 @@ Array [ `); }); - const features = buildOSSFeatures({ savedObjectTypes: ['foo', 'bar'], includeTimelion: true }); + it('returns features including reporting subfeatures', () => { + expect( + buildOSSFeatures({ + savedObjectTypes: ['foo', 'bar'], + includeTimelion: false, + includeReporting: true, + }).map(({ id, subFeatures }) => ({ id, subFeatures })) + ).toMatchSnapshot(); + }); + + it('returns features excluding reporting subfeatures', () => { + expect( + buildOSSFeatures({ + savedObjectTypes: ['foo', 'bar'], + includeTimelion: false, + includeReporting: false, + }).map(({ id, subFeatures }) => ({ id, subFeatures })) + ).toMatchSnapshot(); + }); + + const features = buildOSSFeatures({ + savedObjectTypes: ['foo', 'bar'], + includeTimelion: true, + includeReporting: false, + }); features.forEach((featureConfig) => { (['enterprise', 'basic'] as LicenseType[]).forEach((licenseType) => { describe(`with a ${licenseType} license`, () => { diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index 91839e511a1ad..d1e96b5a788ec 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -6,15 +6,20 @@ */ import { i18n } from '@kbn/i18n'; -import { KibanaFeatureConfig } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; +import type { KibanaFeatureConfig, SubFeatureConfig } from '../common'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; includeTimelion: boolean; + includeReporting: boolean; } -export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSSFeaturesParams) => { +export const buildOSSFeatures = ({ + savedObjectTypes, + includeTimelion, + includeReporting, +}: BuildOSSFeaturesParams): KibanaFeatureConfig[] => { return [ { id: 'discover', @@ -23,6 +28,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }), management: { kibana: ['search_sessions'], + ...(includeReporting ? { insightsAndAlerting: ['reporting'] } : {}), }, order: 100, category: DEFAULT_APP_CATEGORIES.kibana, @@ -107,6 +113,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + ...(includeReporting ? [reportingFeatures.discoverReporting] : []), ], }, { @@ -114,6 +121,9 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS name: i18n.translate('xpack.features.visualizeFeatureName', { defaultMessage: 'Visualize Library', }), + management: { + ...(includeReporting ? { insightsAndAlerting: ['reporting'] } : {}), + }, order: 700, category: DEFAULT_APP_CATEGORIES.kibana, app: ['visualize', 'lens', 'kibana'], @@ -166,6 +176,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + ...(includeReporting ? [reportingFeatures.visualizeReporting] : []), ], }, { @@ -175,6 +186,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }), management: { kibana: ['search_sessions'], + ...(includeReporting ? { insightsAndAlerting: ['reporting'] } : {}), }, order: 200, category: DEFAULT_APP_CATEGORIES.kibana, @@ -279,6 +291,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, ], }, + ...(includeReporting ? [reportingFeatures.dashboardReporting] : []), ], }, { @@ -468,3 +481,99 @@ const timelionFeature: KibanaFeatureConfig = { }, }, }; + +const reportingPrivilegeGroupName = i18n.translate( + 'xpack.features.ossFeatures.reporting.reportingTitle', + { + defaultMessage: 'Reporting', + } +); + +const reportingFeatures: { + discoverReporting: SubFeatureConfig; + dashboardReporting: SubFeatureConfig; + visualizeReporting: SubFeatureConfig; +} = { + discoverReporting: { + name: reportingPrivilegeGroupName, + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'generate_report', + name: i18n.translate('xpack.features.ossFeatures.reporting.discoverGenerateCSV', { + defaultMessage: 'Generate CSV reports', + }), + includeIn: 'all', + savedObject: { all: [], read: [] }, + management: { insightsAndAlerting: ['reporting'] }, + api: ['generateReport'], + ui: ['generateCsv'], + }, + ], + }, + ], + }, + dashboardReporting: { + name: reportingPrivilegeGroupName, + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'generate_report', + name: i18n.translate( + 'xpack.features.ossFeatures.reporting.dashboardGenerateScreenshot', + { + defaultMessage: 'Generate PDF or PNG reports', + } + ), + includeIn: 'all', + minimumLicense: 'platinum', + savedObject: { all: [], read: [] }, + management: { insightsAndAlerting: ['reporting'] }, + api: ['generateReport'], + ui: ['generateScreenshot'], + }, + { + id: 'download_csv_report', + name: i18n.translate('xpack.features.ossFeatures.reporting.dashboardDownloadCSV', { + defaultMessage: 'Download CSV reports from Saved Search panels', + }), + includeIn: 'all', + savedObject: { all: [], read: [] }, + management: { insightsAndAlerting: ['reporting'] }, + api: ['downloadCsv'], + ui: ['downloadCsv'], + }, + ], + }, + ], + }, + visualizeReporting: { + name: reportingPrivilegeGroupName, + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'generate_report', + name: i18n.translate( + 'xpack.features.ossFeatures.reporting.visualizeGenerateScreenshot', + { + defaultMessage: 'Generate PDF or PNG reports', + } + ), + includeIn: 'all', + minimumLicense: 'platinum', + savedObject: { all: [], read: [] }, + management: { insightsAndAlerting: ['reporting'] }, + api: ['generateReport'], + ui: ['generateScreenshot'], + }, + ], + }, + ], + }, +}; diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 6a9fd1da826a6..09a5b78ad868a 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -46,6 +46,14 @@ export interface PluginSetupContract { * */ getElasticsearchFeatures(): ElasticsearchFeature[]; getFeaturesUICapabilities(): UICapabilities; + + /* + * In the future, OSS features should register their own subfeature + * privileges. This can be done when parts of Reporting are moved to + * src/plugins. For now, this method exists for `reporting` to tell + * `features` to include Reporting when registering OSS features. + */ + enableReportingUiCapabilities(): void; } export interface PluginStartContract { @@ -66,6 +74,7 @@ export class FeaturesPlugin private readonly logger: Logger; private readonly featureRegistry: FeatureRegistry = new FeatureRegistry(); private isTimelionEnabled: boolean = false; + private isReportingEnabled: boolean = false; constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get(); @@ -100,6 +109,7 @@ export class FeaturesPlugin this.featureRegistry ), getFeaturesUICapabilities, + enableReportingUiCapabilities: this.enableReportingUiCapabilities.bind(this), }); } @@ -128,10 +138,18 @@ export class FeaturesPlugin const features = buildOSSFeatures({ savedObjectTypes, includeTimelion: this.isTimelionEnabled, + includeReporting: this.isReportingEnabled, }); for (const feature of features) { this.featureRegistry.registerKibanaFeature(feature); } } + + private enableReportingUiCapabilities() { + this.logger.debug( + `Feature controls for Reporting plugin are enabled. Please assign access to Reporting use Kibana feature controls for applications.` + ); + this.isReportingEnabled = true; + } } diff --git a/x-pack/plugins/reporting/public/index.ts b/x-pack/plugins/reporting/public/index.ts index ee9db75da1523..7c82fda7554d4 100644 --- a/x-pack/plugins/reporting/public/index.ts +++ b/x-pack/plugins/reporting/public/index.ts @@ -18,6 +18,7 @@ export interface ReportingSetup { }; getDefaultLayoutSelectors: typeof getDefaultLayoutSelectors; ReportingAPIClient: typeof ReportingAPIClient; + usesUiCapabilities: () => boolean; } export type ReportingStart = ReportingSetup; diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts index 06d626a4c4044..dbd0421fdf9b0 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.test.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { of } from 'rxjs'; +import * as Rx from 'rxjs'; import { first } from 'rxjs/operators'; +import { CoreStart } from 'src/core/public'; import { LicensingPluginSetup } from '../../../licensing/public'; -import { GetCsvReportPanelAction } from './get_csv_panel_action'; +import { ReportingCsvPanelAction } from './get_csv_panel_action'; type LicenseResults = 'valid' | 'invalid' | 'unavailable' | 'expired'; @@ -17,6 +18,8 @@ describe('GetCsvReportPanelAction', () => { let context: any; let mockLicense$: any; let mockSearchSource: any; + let mockStartServicesPayload: [CoreStart, object, unknown]; + let mockStartServices$: Rx.Subject; beforeAll(() => { if (typeof window.URL.revokeObjectURL === 'undefined') { @@ -30,11 +33,20 @@ describe('GetCsvReportPanelAction', () => { beforeEach(() => { mockLicense$ = (state: LicenseResults = 'valid') => { - return (of({ + return (Rx.of({ check: jest.fn().mockImplementation(() => ({ state })), }) as unknown) as LicensingPluginSetup['license$']; }; + mockStartServices$ = new Rx.Subject<[CoreStart, object, unknown]>(); + mockStartServicesPayload = [ + ({ + application: { capabilities: { dashboard: { downloadCsv: true } } }, + } as unknown) as CoreStart, + {}, + null, + ]; + core = { http: { post: jest.fn().mockImplementation(() => Promise.resolve(true)), @@ -78,7 +90,14 @@ describe('GetCsvReportPanelAction', () => { }); it('translates empty embeddable context into job params', async () => { - const panel = new GetCsvReportPanelAction(core, mockLicense$()); + const panel = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); await panel.execute(context); @@ -91,7 +110,6 @@ describe('GetCsvReportPanelAction', () => { }); it('translates embeddable context into job params', async () => { - // setup mockSearchSource = { createCopy: () => mockSearchSource, removeField: jest.fn(), @@ -106,9 +124,15 @@ describe('GetCsvReportPanelAction', () => { }; }; - const panel = new GetCsvReportPanelAction(core, mockLicense$()); + const panel = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); - // test await panel.execute(context); expect(core.http.post).toHaveBeenCalledWith( @@ -121,7 +145,14 @@ describe('GetCsvReportPanelAction', () => { }); it('allows downloading for valid licenses', async () => { - const panel = new GetCsvReportPanelAction(core, mockLicense$()); + const panel = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); await panel.execute(context); @@ -129,7 +160,14 @@ describe('GetCsvReportPanelAction', () => { }); it('shows a good old toastie when it successfully starts', async () => { - const panel = new GetCsvReportPanelAction(core, mockLicense$()); + const panel = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); await panel.execute(context); @@ -144,7 +182,14 @@ describe('GetCsvReportPanelAction', () => { post: jest.fn().mockImplementation(() => Promise.reject('No more ram!')), }, }; - const panel = new GetCsvReportPanelAction(coreFails, mockLicense$()); + const panel = new ReportingCsvPanelAction({ + core: coreFails, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); await panel.execute(context); @@ -152,15 +197,76 @@ describe('GetCsvReportPanelAction', () => { }); it(`doesn't allow downloads with bad licenses`, async () => { - const licenseMock = mockLicense$('invalid'); - const plugin = new GetCsvReportPanelAction(core, licenseMock); - await licenseMock.pipe(first()).toPromise(); + const licenseMock$ = mockLicense$('invalid'); + const plugin = new ReportingCsvPanelAction({ + core, + license$: licenseMock$, + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); + + await licenseMock$.pipe(first()).toPromise(); + expect(await plugin.isCompatible(context)).toEqual(false); }); it('sets a display and icon type', () => { - const panel = new GetCsvReportPanelAction(core, mockLicense$()); + const panel = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); + expect(panel.getIconType()).toMatchInlineSnapshot(`"document"`); expect(panel.getDisplayName()).toMatchInlineSnapshot(`"Download CSV"`); }); + + describe('Application UI Capabilities', () => { + it(`doesn't allow downloads when UI capability is not enabled`, async () => { + const plugin = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next([ + ({ application: { capabilities: {} } } as unknown) as CoreStart, + {}, + null, + ]); + + expect(await plugin.isCompatible(context)).toEqual(false); + }); + + it(`allows downloads when license is valid and UI capability is enabled`, async () => { + mockStartServices$ = new Rx.Subject(); + const plugin = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: true, + }); + + mockStartServices$.next(mockStartServicesPayload); + + expect(await plugin.isCompatible(context)).toEqual(true); + }); + + it(`allows download when license is valid and deprecated roles config is enabled`, async () => { + const plugin = new ReportingCsvPanelAction({ + core, + license$: mockLicense$(), + startServices$: mockStartServices$, + usesUiCapabilities: false, + }); + + expect(await plugin.isCompatible(context)).toEqual(true); + }); + }); }); diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index 95d193880975c..8a863e1ceaa65 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -7,7 +7,9 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; +import * as Rx from 'rxjs'; import type { CoreSetup } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import type { ISearchEmbeddable, SavedSearch } from '../../../../../src/plugins/discover/public'; import { loadSharingDataHelpers, @@ -32,22 +34,38 @@ interface ActionContext { embeddable: ISearchEmbeddable; } -export class GetCsvReportPanelAction implements ActionDefinition { +interface Params { + core: CoreSetup; + startServices$: Rx.Observable<[CoreStart, object, unknown]>; + license$: LicensingPluginSetup['license$']; + usesUiCapabilities: boolean; +} + +export class ReportingCsvPanelAction implements ActionDefinition { private isDownloading: boolean; public readonly type = ''; public readonly id = CSV_REPORTING_ACTION; - private canDownloadCSV: boolean = false; + private licenseHasDownloadCsv: boolean = false; + private capabilityHasDownloadCsv: boolean = false; private core: CoreSetup; - constructor(core: CoreSetup, license$: LicensingPluginSetup['license$']) { + constructor({ core, startServices$, license$, usesUiCapabilities }: Params) { this.isDownloading = false; this.core = core; license$.subscribe((license) => { const results = license.check('reporting', 'basic'); const { showLinks } = checkLicense(results); - this.canDownloadCSV = showLinks; + this.licenseHasDownloadCsv = showLinks; }); + + if (usesUiCapabilities) { + startServices$.subscribe(([{ application }]) => { + this.capabilityHasDownloadCsv = application.capabilities.dashboard?.downloadCsv === true; + }); + } else { + this.capabilityHasDownloadCsv = true; // deprecated + } } public getIconType() { @@ -70,7 +88,7 @@ export class GetCsvReportPanelAction implements ActionDefinition } public isCompatible = async (context: ActionContext) => { - if (!this.canDownloadCSV) { + if (!this.licenseHasDownloadCsv || !this.capabilityHasDownloadCsv) { return false; } @@ -82,7 +100,7 @@ export class GetCsvReportPanelAction implements ActionDefinition public execute = async (context: ActionContext) => { const { embeddable } = context; - if (!isSavedSearchEmbeddable(embeddable)) { + if (!isSavedSearchEmbeddable(embeddable) || !(await this.isCompatible(context))) { throw new IncompatibleActionError(); } @@ -93,6 +111,10 @@ export class GetCsvReportPanelAction implements ActionDefinition const savedSearch = embeddable.getSavedSearch(); const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable); + // If the TZ is set to the default "Browser", it will not be useful for + // server-side export. We need to derive the timezone and pass it as a param + // to the export API. + // TODO: create a helper utility in Reporting. This is repeated in a few places. const kibanaTimezone = this.core.uiSettings.get('dateFormat:tz'); const browserTimezone = kibanaTimezone === 'Browser' ? moment.tz.guess() : kibanaTimezone; const immediateJobParams: JobParamsDownloadCSV = { diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index 435291e76ac49..ff0d425faf54a 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -35,17 +35,13 @@ import { } from './components'; import { ReportingAPIClient } from './lib/reporting_api_client'; import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; -import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; -import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; -import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; +import { ReportingCsvPanelAction } from './panel_actions/get_csv_panel_action'; +import { ReportingCsvShareProvider } from './share_context_menu/register_csv_reporting'; +import { reportingScreenshotShareProvider } from './share_context_menu/register_pdf_png_reporting'; export interface ClientConfigType { - poll: { - jobsRefresh: { - interval: number; - intervalErrorMultiplier: number; - }; - }; + poll: { jobsRefresh: { interval: number; intervalErrorMultiplier: number } }; + roles: { enabled: boolean }; } function getStored(): JobId[] { @@ -90,11 +86,7 @@ export class ReportingPublicPlugin ReportingPublicPluginSetupDendencies, ReportingPublicPluginStartDendencies > { - private readonly contract: ReportingStart = { - components: { ScreenCapturePanel }, - getDefaultLayoutSelectors, - ReportingAPIClient, - }; + private readonly contract: ReportingStart; private readonly stop$ = new Rx.ReplaySubject(1); private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', @@ -106,22 +98,30 @@ export class ReportingPublicPlugin constructor(initializerContext: PluginInitializerContext) { this.config = initializerContext.config.get(); + + this.contract = { + ReportingAPIClient, + components: { ScreenCapturePanel }, + getDefaultLayoutSelectors, + usesUiCapabilities: () => this.config.roles?.enabled === false, + }; } - public setup( - core: CoreSetup, - { home, management, licensing, uiActions, share }: ReportingPublicPluginSetupDendencies - ) { + public setup(core: CoreSetup, setupDeps: ReportingPublicPluginSetupDendencies) { + const { http, notifications, getStartServices, uiSettings } = core; + const { toasts } = notifications; const { - http, - notifications: { toasts }, - getStartServices, - uiSettings, - } = core; - const { license$ } = licensing; + home, + management, + licensing: { license$ }, + share, + uiActions, + } = setupDeps; + + const startServices$ = Rx.from(getStartServices()); + const usesUiCapabilities = !this.config.roles.enabled; const apiClient = new ReportingAPIClient(http); - const action = new GetCsvReportPanelAction(core, license$); home.featureCatalogue.register({ id: 'reporting', @@ -136,6 +136,7 @@ export class ReportingPublicPlugin showOnHomePage: false, category: FeatureCatalogueCategory.ADMIN, }); + management.sections.section.insightsAndAlerting.registerApp({ id: 'reporting', title: this.title, @@ -157,15 +158,29 @@ export class ReportingPublicPlugin }, }); - uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, action); + uiActions.addTriggerAction( + CONTEXT_MENU_TRIGGER, + new ReportingCsvPanelAction({ core, startServices$, license$, usesUiCapabilities }) + ); - share.register(csvReportingProvider({ apiClient, toasts, license$, uiSettings })); share.register( - reportingPDFPNGProvider({ + ReportingCsvShareProvider({ + apiClient, + toasts, + license$, + startServices$, + uiSettings, + usesUiCapabilities, + }) + ); + share.register( + reportingScreenshotShareProvider({ apiClient, toasts, license$, + startServices$, uiSettings, + usesUiCapabilities, }) ); diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 8995ef4739b09..9d26c69e57297 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -8,7 +8,9 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; +import * as Rx from 'rxjs'; import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import type { SearchSourceFields } from 'src/plugins/data/common'; import type { ShareContext } from '../../../../../src/plugins/share/public'; import type { LicensingPluginSetup } from '../../../licensing/public'; @@ -18,34 +20,46 @@ import { ReportingPanelContent } from '../components/reporting_panel_content_laz import { checkLicense } from '../lib/license_check'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; -interface ReportingProvider { - apiClient: ReportingAPIClient; - toasts: ToastsSetup; - license$: LicensingPluginSetup['license$']; - uiSettings: IUiSettingsClient; -} - -export const csvReportingProvider = ({ +export const ReportingCsvShareProvider = ({ apiClient, toasts, license$, + startServices$, uiSettings, -}: ReportingProvider) => { - let toolTipContent = ''; - let disabled = true; - let hasCSVReporting = false; + usesUiCapabilities, +}: { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + license$: LicensingPluginSetup['license$']; + startServices$: Rx.Observable<[CoreStart, object, unknown]>; + uiSettings: IUiSettingsClient; + usesUiCapabilities: boolean; +}) => { + let licenseToolTipContent = ''; + let licenseHasCsvReporting = false; + let licenseDisabled = true; + let capabilityHasCsvReporting = false; license$.subscribe((license) => { - const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'basic')); - - toolTipContent = message; - hasCSVReporting = showLinks; - disabled = !enableLinks; + const licenseCheck = checkLicense(license.check('reporting', 'basic')); + licenseToolTipContent = licenseCheck.message; + licenseHasCsvReporting = licenseCheck.showLinks; + licenseDisabled = !licenseCheck.enableLinks; }); + if (usesUiCapabilities) { + startServices$.subscribe(([{ application }]) => { + // TODO: add abstractions in ExportTypeRegistry to use here? + capabilityHasCsvReporting = application.capabilities.discover?.generateCsv === true; + }); + } else { + capabilityHasCsvReporting = true; // deprecated + } + // If the TZ is set to the default "Browser", it will not be useful for // server-side export. We need to derive the timezone and pass it as a param // to the export API. + // TODO: create a helper utility in Reporting. This is repeated in a few places. const browserTimezone = uiSettings.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() @@ -74,7 +88,7 @@ export const csvReportingProvider = ({ const shareActions = []; - if (hasCSVReporting) { + if (licenseHasCsvReporting && capabilityHasCsvReporting) { const panelTitle = i18n.translate('xpack.reporting.shareContextMenu.csvReportsButtonLabel', { defaultMessage: 'CSV Reports', }); @@ -83,8 +97,8 @@ export const csvReportingProvider = ({ shareMenuItem: { name: panelTitle, icon: 'document', - toolTipContent, - disabled, + toolTipContent: licenseToolTipContent, + disabled: licenseDisabled, ['data-test-subj']: 'csvReportMenuItem', sortOrder: 1, }, diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 00ba167c50ae6..f4a952ef58298 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -8,7 +8,9 @@ import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import React from 'react'; +import * as Rx from 'rxjs'; import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; +import { CoreStart } from 'src/core/public'; import type { ShareContext } from '../../../../../src/plugins/share/public'; import type { LicensingPluginSetup } from '../../../licensing/public'; import type { LayoutParams } from '../../common/types'; @@ -18,34 +20,100 @@ import { ScreenCapturePanelContent } from '../components/screen_capture_panel_co import { checkLicense } from '../lib/license_check'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; -interface ReportingPDFPNGProvider { +interface JobParamsProviderOptions { + shareableUrl: string; apiClient: ReportingAPIClient; - toasts: ToastsSetup; - license$: LicensingPluginSetup['license$']; - uiSettings: IUiSettingsClient; + objectType: string; + browserTimezone: string; + sharingData: Record; } -export const reportingPDFPNGProvider = ({ +const jobParamsProvider = ({ + objectType, + browserTimezone, + sharingData, +}: JobParamsProviderOptions) => { + return { + objectType, + browserTimezone, + layout: sharingData.layout as LayoutParams, + title: sharingData.title as string, + }; +}; + +const getPdfJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPDF => { + // Relative URL must have URL prefix (Spaces ID prefix), but not server basePath + // Replace hashes with original RISON values. + const relativeUrl = opts.shareableUrl.replace( + window.location.origin + opts.apiClient.getServerBasePath(), + '' + ); + + return { + ...jobParamsProvider(opts), + relativeUrls: [relativeUrl], // multi URL for PDF + }; +}; + +const getPngJobParams = (opts: JobParamsProviderOptions) => (): JobParamsPNG => { + // Replace hashes with original RISON values. + const relativeUrl = opts.shareableUrl.replace( + window.location.origin + opts.apiClient.getServerBasePath(), + '' + ); + + return { + ...jobParamsProvider(opts), + relativeUrl, // single URL for PNG + }; +}; + +export const reportingScreenshotShareProvider = ({ apiClient, toasts, license$, + startServices$, uiSettings, -}: ReportingPDFPNGProvider) => { - let toolTipContent = ''; - let disabled = true; - let hasPDFPNGReporting = false; + usesUiCapabilities, +}: { + apiClient: ReportingAPIClient; + toasts: ToastsSetup; + license$: LicensingPluginSetup['license$']; + startServices$: Rx.Observable<[CoreStart, object, unknown]>; + uiSettings: IUiSettingsClient; + usesUiCapabilities: boolean; +}) => { + let licenseToolTipContent = ''; + let licenseDisabled = true; + let licenseHasScreenshotReporting = false; + let capabilityHasDashboardScreenshotReporting = false; + let capabilityHasVisualizeScreenshotReporting = false; license$.subscribe((license) => { const { enableLinks, showLinks, message } = checkLicense(license.check('reporting', 'gold')); - - toolTipContent = message; - hasPDFPNGReporting = showLinks; - disabled = !enableLinks; + licenseToolTipContent = message; + licenseHasScreenshotReporting = showLinks; + licenseDisabled = !enableLinks; }); + if (usesUiCapabilities) { + startServices$.subscribe(([{ application }]) => { + // TODO: add abstractions in ExportTypeRegistry to use here? + capabilityHasDashboardScreenshotReporting = + application.capabilities.dashboard?.generateScreenshot === true; + capabilityHasVisualizeScreenshotReporting = + application.capabilities.visualize?.generateScreenshot === true; + }); + } else { + // deprecated + capabilityHasDashboardScreenshotReporting = true; + capabilityHasVisualizeScreenshotReporting = true; + } + // If the TZ is set to the default "Browser", it will not be useful for // server-side export. We need to derive the timezone and pass it as a param // to the export API. + // TODO: create a helper utility in Reporting. This is repeated in a few places. const browserTimezone = uiSettings.get('dateFormat:tz') === 'Browser' ? moment.tz.guess() @@ -59,124 +127,100 @@ export const reportingPDFPNGProvider = ({ onClose, shareableUrl, }: ShareContext) => { - if (!['dashboard', 'visualization'].includes(objectType)) { + if (!licenseHasScreenshotReporting) { return []; } - // Dashboard only mode does not currently support reporting - // https://github.com/elastic/kibana/issues/18286 - // @TODO For NP - if (objectType === 'dashboard' && false) { + + if (!['dashboard', 'visualization'].includes(objectType)) { return []; } - const getPdfJobParams = (): JobParamsPDF => { - // Relative URL must have URL prefix (Spaces ID prefix), but not server basePath - // Replace hashes with original RISON values. - const relativeUrl = shareableUrl.replace( - window.location.origin + apiClient.getServerBasePath(), - '' - ); - - return { - objectType, - browserTimezone, - relativeUrls: [relativeUrl], // multi URL for PDF - layout: sharingData.layout as LayoutParams, - title: sharingData.title as string, - }; - }; + if (objectType === 'dashboard' && !capabilityHasDashboardScreenshotReporting) { + return []; + } - const getPngJobParams = (): JobParamsPNG => { - // Replace hashes with original RISON values. - const relativeUrl = shareableUrl.replace( - window.location.origin + apiClient.getServerBasePath(), - '' - ); - - return { - objectType, - browserTimezone, - relativeUrl, // single URL for PNG - layout: sharingData.layout as LayoutParams, - title: sharingData.title as string, - }; - }; + if (objectType === 'visualize' && !capabilityHasVisualizeScreenshotReporting) { + return []; + } const shareActions = []; - if (hasPDFPNGReporting) { - const pngPanelTitle = i18n.translate( - 'xpack.reporting.shareContextMenu.pngReportsButtonLabel', - { - defaultMessage: 'PNG Reports', - } - ); - - const pdfPanelTitle = i18n.translate( - 'xpack.reporting.shareContextMenu.pdfReportsButtonLabel', - { - defaultMessage: 'PDF Reports', - } - ); - - shareActions.push({ - shareMenuItem: { - name: pngPanelTitle, - icon: 'document', - toolTipContent, - disabled, - ['data-test-subj']: 'pngReportMenuItem', - sortOrder: 10, - }, - panel: { - id: 'reportingPngPanel', - title: pngPanelTitle, - content: ( - - ), - }, - }); - - shareActions.push({ - shareMenuItem: { - name: pdfPanelTitle, - icon: 'document', - toolTipContent, - disabled, - ['data-test-subj']: 'pdfReportMenuItem', - sortOrder: 10, - }, - panel: { - id: 'reportingPdfPanel', - title: pdfPanelTitle, - content: ( - - ), - }, - }); - } + const pngPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pngReportsButtonLabel', { + defaultMessage: 'PNG Reports', + }); + + const panelPng = { + shareMenuItem: { + name: pngPanelTitle, + icon: 'document', + toolTipContent: licenseToolTipContent, + disabled: licenseDisabled, + ['data-test-subj']: 'pngReportMenuItem', + sortOrder: 10, + }, + panel: { + id: 'reportingPngPanel', + title: pngPanelTitle, + content: ( + + ), + }, + }; + const pdfPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pdfReportsButtonLabel', { + defaultMessage: 'PDF Reports', + }); + + const panelPdf = { + shareMenuItem: { + name: pdfPanelTitle, + icon: 'document', + toolTipContent: licenseToolTipContent, + disabled: licenseDisabled, + ['data-test-subj']: 'pdfReportMenuItem', + sortOrder: 10, + }, + panel: { + id: 'reportingPdfPanel', + title: pdfPanelTitle, + content: ( + + ), + }, + }; + + shareActions.push(panelPng); + shareActions.push(panelPdf); return shareActions; }; - return { - id: 'screenCaptureReports', - getShareMenuItems, - }; + return { id: 'screenCaptureReports', getShareMenuItems }; }; diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index 6dfe890e173a1..e57528e5be490 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -104,6 +104,9 @@ describe('Reporting server createConfig$', () => { "pollInterval": 3000, "timeout": 120000, }, + "roles": Object { + "enabled": false, + }, } `); expect((mockLogger.warn as any).mock.calls.length).toBe(0); diff --git a/x-pack/plugins/reporting/server/config/index.test.ts b/x-pack/plugins/reporting/server/config/index.test.ts index a395cd23288eb..cba64500575aa 100644 --- a/x-pack/plugins/reporting/server/config/index.test.ts +++ b/x-pack/plugins/reporting/server/config/index.test.ts @@ -32,7 +32,7 @@ const applyReportingDeprecations = (settings: Record = {}) => { describe('deprecations', () => { ['.foo', '.reporting'].forEach((index) => { it('logs a warning if index is set', () => { - const { messages } = applyReportingDeprecations({ index }); + const { messages } = applyReportingDeprecations({ index, roles: { enabled: false } }); expect(messages).toMatchInlineSnapshot(` Array [ "\\"xpack.reporting.index\\" is deprecated. Multitenancy by changing \\"kibana.index\\" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details", @@ -40,4 +40,18 @@ describe('deprecations', () => { `); }); }); + + it('logs a warning if roles.enabled: true is set', () => { + const { messages } = applyReportingDeprecations({ roles: { enabled: true } }); + expect(messages).toMatchInlineSnapshot(` + Array [ + "\\"xpack.reporting.roles\\" is deprecated. Granting reporting privilege through a \\"reporting_user\\" role will not be supported starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privilege to users through feature controls in Management > Security > Roles", + ] + `); + }); + + it('does not log a warning if roles.enabled: false is set', () => { + const { messages } = applyReportingDeprecations({ roles: { enabled: false } }); + expect(messages).toMatchInlineSnapshot(`Array []`); + }); }); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index 4b97dbc1e2a84..cdd395037a410 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -7,14 +7,13 @@ import { PluginConfigDescriptor } from 'kibana/server'; import { get } from 'lodash'; - import { ConfigSchema, ReportingConfigType } from './schema'; export { buildConfig } from './config'; export { registerUiSettings } from './ui_settings'; export { ConfigSchema, ReportingConfigType }; export const config: PluginConfigDescriptor = { - exposeToBrowser: { poll: true }, + exposeToBrowser: { poll: true, roles: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ unused('capture.browser.chromium.maxScreenshotDimension'), @@ -31,6 +30,16 @@ export const config: PluginConfigDescriptor = { message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, }); } + + if (reporting?.roles?.enabled !== false) { + addDeprecation({ + message: + `"${fromPath}.roles" is deprecated. Granting reporting privilege through a "reporting_user" role will not be supported ` + + `starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privilege to users ` + + `through feature controls in Management > Security > Roles`, + }); + } + return settings; }, ], diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 49e740b4f2683..e299db2405125 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -107,6 +107,7 @@ describe('Reporting Config Schema', () => { "allow": Array [ "reporting_user", ], + "enabled": true, }, } `); @@ -211,6 +212,7 @@ describe('Reporting Config Schema', () => { "allow": Array [ "reporting_user", ], + "enabled": true, }, } `); diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 3f901b283f7bd..f56bf5520072b 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -160,6 +160,7 @@ const EncryptionKeySchema = schema.conditional( ); const RolesSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), // true: use ES API for access control (deprecated in 7.x). false: use Kibana API for application features (8.0) allow: schema.arrayOf(schema.string(), { defaultValue: ['reporting_user'] }), }); diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 1e695105ac1ea..02d18fd5489d3 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -12,10 +12,12 @@ import { BasePath, IClusterClient, KibanaRequest, + PluginInitializerContext, SavedObjectsClientContract, SavedObjectsServiceStart, UiSettingsServiceStart, } from '../../../../src/core/server'; +import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -23,12 +25,12 @@ import { DEFAULT_SPACE_ID } from '../../spaces/common/constants'; import { SpacesPluginSetup } from '../../spaces/server'; import { ReportingConfig } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; +import { ReportingConfigType } from './config'; import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; import { ESQueueInstance } from './lib/create_queue'; import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots'; import { ReportingStore } from './lib/store'; -import { ReportingPluginRouter } from './types'; -import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; +import { ReportingPluginRouter, ReportingStart } from './types'; export interface ReportingInternalSetup { basePath: Pick; @@ -37,6 +39,7 @@ export interface ReportingInternalSetup { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; + logger: LevelLogger; } export interface ReportingInternalStart { @@ -47,6 +50,7 @@ export interface ReportingInternalStart { esClient: IClusterClient; data: DataPluginStart; esqueue: ESQueueInstance; + logger: LevelLogger; } export class ReportingCore { @@ -54,10 +58,22 @@ export class ReportingCore { private pluginStartDeps?: ReportingInternalStart; private readonly pluginSetup$ = new Rx.ReplaySubject(); // observe async background setupDeps and config each are done private readonly pluginStart$ = new Rx.ReplaySubject(); // observe async background startDeps + private deprecatedAllowedRoles: string[] | false = false; // DEPRECATED. If `false`, the deprecated features have been disableed private exportTypesRegistry = getExportTypesRegistry(); private config?: ReportingConfig; - constructor(private logger: LevelLogger) {} + public getStartContract: () => ReportingStart; + + constructor(private logger: LevelLogger, context: PluginInitializerContext) { + const syncConfig = context.config.get(); + this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false; + + this.getStartContract = (): ReportingStart => { + return { + usesUiCapabilities: () => syncConfig.roles.enabled === false, + }; + }; + } /* * Register setupDeps @@ -109,23 +125,38 @@ export class ReportingCore { } /** - * Registers reporting as an Elasticsearch feature for the purpose of toggling visibility based on roles. + * If xpack.reporting.roles.enabled === true, register Reporting as a feature + * that is controlled by user role names */ public registerFeature() { - const config = this.getConfig(); - const allowedRoles = ['superuser', ...(config.get('roles')?.allow ?? [])]; - this.getPluginSetupDeps().features.registerElasticsearchFeature({ - id: 'reporting', - catalogue: ['reporting'], - management: { - insightsAndAlerting: ['reporting'], - }, - privileges: allowedRoles.map((role) => ({ + const { features } = this.getPluginSetupDeps(); + const deprecatedRoles = this.getDeprecatedAllowedRoles(); + + if (deprecatedRoles !== false) { + // refer to roles.allow configuration (deprecated path) + const allowedRoles = ['superuser', ...(deprecatedRoles ?? [])]; + const privileges = allowedRoles.map((role) => ({ requiredClusterPrivileges: [], requiredRoles: [role], ui: [], - })), - }); + })); + + // self-register as an elasticsearch feature (deprecated) + features.registerElasticsearchFeature({ + id: 'reporting', + catalogue: ['reporting'], + management: { + insightsAndAlerting: ['reporting'], + }, + privileges, + }); + } else { + this.logger.debug( + `Reporting roles configuration is disabled. Please assign access to Reporting use Kibana feature controls for applications.` + ); + // trigger application to register Reporting as a subfeature + features.enableReportingUiCapabilities(); + } } /* @@ -138,6 +169,15 @@ export class ReportingCore { return this.config; } + /* + * If deprecated feature has not been disabled, + * this returns an array of allowed role names + * that have access to Reporting. + */ + public getDeprecatedAllowedRoles(): string[] | false { + return this.deprecatedAllowedRoles; + } + /* * Gives async access to the startDeps */ diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 18cd6e826b5e7..90d34acf28ea9 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -22,7 +22,7 @@ import { CancellationToken } from '../../../common'; import { CSV_BOM_CHARS } from '../../../common/constants'; import { LevelLogger } from '../../lib'; import { setFieldFormats } from '../../services'; -import { createMockReportingCore } from '../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; import { TaskPayloadDeprecatedCSV } from './types'; @@ -75,7 +75,7 @@ describe('CSV Execute Job', function () { configGetStub.withArgs('csv', 'scroll').returns({}); mockReportingConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; - mockReportingCore = await createMockReportingCore(mockReportingConfig); + mockReportingCore = await createMockReportingCore(createMockConfigSchema()); mockReportingCore.getUiSettingsServiceFactory = () => Promise.resolve((mockUiSettingsClient as unknown) as IUiSettingsClient); mockReportingCore.setConfig(mockReportingConfig); diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts index 1c2e15ebc5d9b..c9d57370ab766 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/execute_job.test.ts @@ -19,7 +19,6 @@ import nodeCrypto from '@elastic/node-crypto'; import { ReportingCore } from '../../'; import { CancellationToken } from '../../../common'; import { - createMockConfig, createMockConfigSchema, createMockLevelLogger, createMockReportingCore, @@ -34,7 +33,9 @@ let reportingCore: ReportingCore; beforeAll(async () => { const crypto = nodeCrypto({ encryptionKey }); - const config = createMockConfig( + + encryptedHeaders = await crypto.encrypt(headers); + reportingCore = await createMockReportingCore( createMockConfigSchema({ encryptionKey, csv: { @@ -45,10 +46,6 @@ beforeAll(async () => { }, }) ); - - encryptedHeaders = await crypto.encrypt(headers); - - reportingCore = await createMockReportingCore(config); }); test('gets the csv content from job parameters', async () => { diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index 34fe5360522b1..ee264f7c57ff6 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -9,7 +9,11 @@ import * as Rx from 'rxjs'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; -import { createMockReportingCore } from '../../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../../test_helpers'; import { generatePngObservableFactory } from '../lib/generate_png'; import { TaskPayloadPNG } from '../types'; import { runTaskFnFactory } from './'; @@ -40,27 +44,17 @@ const encryptHeaders = async (headers: Record) => { const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPNG; beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; - const reportingConfig = { + const mockReportingConfig = createMockConfigSchema({ index: '.reporting-2018.10.10', encryptionKey: mockEncryptionKey, - 'kibanaServer.hostname': 'localhost', - 'kibanaServer.port': 5601, - 'kibanaServer.protocol': 'http', - 'queue.indexInterval': 'daily', - 'queue.timeout': Infinity, - }; - const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, - }; + queue: { + indexInterval: 'daily', + timeout: Infinity, + }, + }); mockReporting = await createMockReportingCore(mockReportingConfig); - - // @ts-ignore over-riding config method - mockReporting.config = mockReportingConfig; + mockReporting.setConfig(createMockConfig(mockReportingConfig)); (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); }); @@ -98,14 +92,14 @@ test(`passes browserTimezone to generatePng`, async () => { ], "warning": [Function], }, - "http://localhost:5601/sbp/app/kibana#/something", + "localhost:80undefined/app/kibana#/something", "UTC", Object { "conditions": Object { - "basePath": "/sbp", + "basePath": undefined, "hostname": "localhost", - "port": 5601, - "protocol": "http", + "port": 80, + "protocol": undefined, }, "headers": Object {}, }, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index d3b2a9be72522..6e0314c2dff56 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -11,11 +11,7 @@ import * as Rx from 'rxjs'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; -import { - createMockConfig, - createMockConfigSchema, - createMockReportingCore, -} from '../../../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; @@ -53,12 +49,7 @@ beforeEach(async () => { 'kibanaServer.protocol': 'http', }; const mockSchema = createMockConfigSchema(reportingConfig); - const mockReportingConfig = createMockConfig(mockSchema); - - mockReporting = await createMockReportingCore(mockReportingConfig); - - // @ts-ignore over-riding config - mockReporting.config = mockReportingConfig; + mockReporting = await createMockReportingCore(mockSchema); (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts index ed58fef2f5dc8..ebdceda0820b9 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/get_custom_logo.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ReportingConfig, ReportingCore } from '../../../'; +import { ReportingCore } from '../../../'; import { createMockConfig, createMockConfigSchema, @@ -15,14 +15,12 @@ import { import { getConditionalHeaders } from '../../common'; import { getCustomLogo } from './get_custom_logo'; -let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; const logger = createMockLevelLogger(); beforeEach(async () => { - mockConfig = createMockConfig(createMockConfigSchema()); - mockReportingPlugin = await createMockReportingCore(mockConfig); + mockReportingPlugin = await createMockReportingCore(createMockConfigSchema()); }); test(`gets logo from uiSettings`, async () => { @@ -42,7 +40,10 @@ test(`gets logo from uiSettings`, async () => { get: mockGet, }); - const conditionalHeaders = getConditionalHeaders(mockConfig, permittedHeaders); + const conditionalHeaders = getConditionalHeaders( + createMockConfig(createMockConfigSchema()), + permittedHeaders + ); const { logo } = await getCustomLogo( mockReportingPlugin, diff --git a/x-pack/plugins/reporting/server/index.ts b/x-pack/plugins/reporting/server/index.ts index 0233e4dfa4ebd..999311b9ae17b 100644 --- a/x-pack/plugins/reporting/server/index.ts +++ b/x-pack/plugins/reporting/server/index.ts @@ -6,17 +6,20 @@ */ import { PluginInitializerContext } from 'kibana/server'; -import { ReportingPlugin } from './plugin'; import { ReportingConfigType } from './config'; +import { ReportingPlugin } from './plugin'; export const plugin = (initContext: PluginInitializerContext) => new ReportingPlugin(initContext); -export { ReportingPlugin as Plugin }; export { config } from './config'; -export { ReportingSetupDeps as PluginSetup } from './types'; -export { ReportingStartDeps as PluginStart } from './types'; - +export { ReportingConfig } from './config/config'; // internal imports export { ReportingCore } from './core'; -export { ReportingConfig } from './config/config'; +export { + ReportingSetup, + ReportingSetupDeps as PluginSetup, + ReportingStartDeps as PluginStart, +} from './types'; + +export { ReportingPlugin as Plugin }; diff --git a/x-pack/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts index 23392ebbb4a8f..448e797acd59d 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts @@ -6,12 +6,11 @@ */ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; -import * as sinon from 'sinon'; import { ElasticsearchClient } from 'kibana/server'; +import * as sinon from 'sinon'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; -import { ReportingConfig, ReportingCore } from '../../server'; +import { ReportingCore } from '../../server'; import { - createMockConfig, createMockConfigSchema, createMockLevelLogger, createMockReportingCore, @@ -38,14 +37,11 @@ const getMockExportTypesRegistry = ( describe('Create Worker', () => { let mockReporting: ReportingCore; - let mockConfig: ReportingConfig; let queue: Esqueue; let client: DeeplyMockedKeys; beforeEach(async () => { - const mockSchema = createMockConfigSchema(reportingConfig); - mockConfig = createMockConfig(mockSchema); - mockReporting = await createMockReportingCore(mockConfig); + mockReporting = await createMockReportingCore(createMockConfigSchema(reportingConfig)); mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); ({ asInternalUser: client } = elasticsearchServiceMock.createClusterClient()); diff --git a/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts new file mode 100644 index 0000000000000..d9d1815835baa --- /dev/null +++ b/x-pack/plugins/reporting/server/lib/enqueue_job.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'src/core/server'; +import { ReportingCore } from '../'; +import { TaskManagerStartContract } from '../../../task_manager/server'; +import { ReportingInternalStart } from '../core'; +import { + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../test_helpers'; +import { BasePayload, ReportingRequestHandlerContext } from '../types'; +import { ExportTypesRegistry, ReportingStore } from './'; +import { enqueueJobFactory } from './enqueue_job'; +import { Report } from './store'; +import { TaskRunResult } from './tasks'; + +describe('Enqueue Job', () => { + const logger = createMockLevelLogger(); + let mockReporting: ReportingCore; + let mockExportTypesRegistry: ExportTypesRegistry; + + beforeAll(async () => { + mockExportTypesRegistry = new ExportTypesRegistry(); + mockExportTypesRegistry.register({ + id: 'printablePdf', + name: 'Printable PDFble', + jobType: 'printable_pdf', + jobContentEncoding: 'base64', + jobContentExtension: 'pdf', + validLicenses: ['turquoise'], + createJobFnFactory: () => async () => + (({ createJobTest: { test1: 'yes' } } as unknown) as BasePayload), + runTaskFnFactory: () => async () => + (({ runParamsTest: { test2: 'yes' } } as unknown) as TaskRunResult), + }); + mockReporting = await createMockReportingCore(createMockConfigSchema()); + mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry; + mockReporting.getStore = () => + Promise.resolve(({ + addReport: jest + .fn() + .mockImplementation( + (report) => new Report({ ...report, _index: '.reporting-foo-index-234' }) + ), + } as unknown) as ReportingStore); + + const scheduleMock = jest.fn().mockImplementation(() => ({ + id: '123-great-id', + })); + + await mockReporting.pluginStart(({ + taskManager: ({ + ensureScheduled: jest.fn(), + schedule: scheduleMock, + } as unknown) as TaskManagerStartContract, + } as unknown) as ReportingInternalStart); + }); + + it('returns a Report object', async () => { + const enqueueJob = enqueueJobFactory(mockReporting, logger); + const report = await enqueueJob( + 'printablePdf', + { + objectType: 'visualization', + title: 'cool-viz', + }, + false, + ({} as unknown) as ReportingRequestHandlerContext, + ({} as unknown) as KibanaRequest + ); + + expect(report).toMatchObject({ + _id: expect.any(String), + _index: '.reporting-foo-index-234', + attempts: 0, + created_by: false, + created_at: expect.any(String), + jobtype: 'printable_pdf', + meta: { objectType: 'visualization' }, + output: null, + payload: { createJobTest: { test1: 'yes' } }, + status: 'pending', + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index 3be54aa8ab8b5..a2c6343eaafb9 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,9 +7,8 @@ import type { DeeplyMockedKeys } from '@kbn/utility-types/jest'; import { ElasticsearchClient } from 'src/core/server'; -import { ReportingConfig, ReportingCore } from '../..'; +import { ReportingCore } from '../..'; import { - createMockConfig, createMockConfigSchema, createMockLevelLogger, createMockReportingCore, @@ -19,7 +18,6 @@ import { ReportingStore } from './store'; describe('ReportingStore', () => { const mockLogger = createMockLevelLogger(); - let mockConfig: ReportingConfig; let mockCore: ReportingCore; let mockEsClient: DeeplyMockedKeys; @@ -28,9 +26,7 @@ describe('ReportingStore', () => { index: '.reporting-test', queue: { indexInterval: 'week' }, }; - const mockSchema = createMockConfigSchema(reportingConfig); - mockConfig = createMockConfig(mockSchema); - mockCore = await createMockReportingCore(mockConfig); + mockCore = await createMockReportingCore(createMockConfigSchema(reportingConfig)); mockEsClient = (await mockCore.getEsClient()).asInternalUser as typeof mockEsClient; mockEsClient.indices.create.mockResolvedValue({} as any); @@ -71,9 +67,7 @@ describe('ReportingStore', () => { index: '.reporting-test', queue: { indexInterval: 'centurially' }, }; - const mockSchema = createMockConfigSchema(reportingConfig); - mockConfig = createMockConfig(mockSchema); - mockCore = await createMockReportingCore(mockConfig); + mockCore = await createMockReportingCore(createMockConfigSchema(reportingConfig)); const store = new ReportingStore(mockCore, mockLogger); const mockReport = new Report({ diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index c21bc7376b0b3..c6868782f8cdd 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -65,23 +65,6 @@ describe('Reporting Plugin', () => { expect(plugin.setup(coreSetup, pluginSetup)).not.toHaveProperty('then'); }); - it('logs setup issues', async () => { - initContext.config = null; - const plugin = new ReportingPlugin(initContext); - // @ts-ignore overloading error logger - plugin.logger.error = jest.fn(); - plugin.setup(coreSetup, pluginSetup); - - await sleep(5); - - // @ts-ignore overloading error logger - expect(plugin.logger.error.mock.calls[0][0]).toMatch( - /Error in Reporting setup, reporting may not function properly/ - ); - // @ts-ignore overloading error logger - expect(plugin.logger.error).toHaveBeenCalledTimes(2); - }); - it('has a sync startup process', async () => { const plugin = new ReportingPlugin(initContext); plugin.setup(coreSetup, pluginSetup); diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 8bc65421731b5..7794bfd307e7c 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -24,23 +24,23 @@ import { registerReportingUsageCollector } from './usage'; export class ReportingPlugin implements Plugin { - private readonly initializerContext: PluginInitializerContext; private logger: LevelLogger; - private reportingCore: ReportingCore; + private reportingCore?: ReportingCore; - constructor(context: PluginInitializerContext) { - this.logger = new LevelLogger(context.logger.get()); - this.reportingCore = new ReportingCore(this.logger); - this.initializerContext = context; + constructor(private initContext: PluginInitializerContext) { + this.logger = new LevelLogger(initContext.logger.get()); } public setup(core: CoreSetup, plugins: ReportingSetupDeps) { + const reportingCore = new ReportingCore(this.logger, this.initContext); + // prevent throwing errors in route handlers about async deps not being initialized // @ts-expect-error null is not assignable to object. use a boolean property to ensure reporting API is enabled. core.http.registerRouteHandlerContext(PLUGIN_ID, () => { - if (this.reportingCore.pluginIsStarted()) { - return {}; // ReportingStart contract + if (reportingCore.pluginIsStarted()) { + return reportingCore.getStartContract(); } else { + this.logger.error(`Reporting features are not yet ready`); return null; } }); @@ -49,7 +49,6 @@ export class ReportingPlugin const { http } = core; const { features, licensing, security, spaces } = plugins; - const { initializerContext: initContext, reportingCore } = this; const router = http.createRouter(); const basePath = http.basePath; @@ -61,6 +60,7 @@ export class ReportingPlugin router, security, spaces, + logger: this.logger, }); registerReportingUsageCollector(reportingCore, plugins); @@ -68,7 +68,7 @@ export class ReportingPlugin // async background setup (async () => { - const config = await buildConfig(initContext, core, this.logger); + const config = await buildConfig(this.initContext, core, this.logger); reportingCore.setConfig(config); // Feature registration relies on config, so it cannot be setup before here. reportingCore.registerFeature(); @@ -78,26 +78,26 @@ export class ReportingPlugin this.logger.error(e); }); - return {}; + this.reportingCore = reportingCore; + return reportingCore.getStartContract(); } public start(core: CoreStart, plugins: ReportingStartDeps) { // use data plugin for csv formats setFieldFormats(plugins.data.fieldFormats); - - const { logger, reportingCore } = this; + const reportingCore = this.reportingCore!; // async background start (async () => { - await this.reportingCore.pluginSetsUp(); + await reportingCore.pluginSetsUp(); const config = reportingCore.getConfig(); - const browserDriverFactory = await initializeBrowserDriverFactory(config, logger); - const store = new ReportingStore(reportingCore, logger); + const browserDriverFactory = await initializeBrowserDriverFactory(config, this.logger); + const store = new ReportingStore(reportingCore, this.logger); const esqueue = await createQueueFactory( reportingCore, store, - logger, + this.logger, core.elasticsearch.client.asInternalUser ); // starts polling for pending jobs @@ -109,6 +109,7 @@ export class ReportingPlugin esClient: core.elasticsearch.client, data: plugins.data, esqueue, + logger: this.logger, }); this.logger.debug('Start complete'); @@ -117,6 +118,6 @@ export class ReportingPlugin this.logger.error(e); }); - return {}; + return reportingCore.getStartContract(); } } diff --git a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts index 5d2b77c082ca5..2da509f024c25 100644 --- a/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts +++ b/x-pack/plugins/reporting/server/routes/csv_searchsource_immediate.ts @@ -38,6 +38,13 @@ export function registerGenerateCsvFromSavedObjectImmediate( const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; + // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new + // public method to return this array + // const registry = reporting.getExportTypesRegistry(); + // const kibanaAccessControlTags = registry.getAllAccessControlTags(); + const useKibanaAccessControl = reporting.getDeprecatedAllowedRoles() === false; // true if deprecated config is turned off + const kibanaAccessControlTags = useKibanaAccessControl ? ['access:downloadCsv'] : []; + // This API calls run the SearchSourceImmediate export type's runTaskFn directly router.post( { @@ -50,6 +57,9 @@ export function registerGenerateCsvFromSavedObjectImmediate( title: schema.string(), }), }, + options: { + tags: kibanaAccessControlTags, + }, }, userHandler(async (user, context, req: CsvFromSavedObjectRequest, res) => { const logger = parentLogger.clone(['csv_searchsource_immediate']); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index 25713c2acc9e4..9c541427049ec 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -12,12 +12,13 @@ import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../..'; import { + createMockConfigSchema, createMockLevelLogger, createMockPluginSetup, createMockReportingCore, } from '../../test_helpers'; -import { registerDiagnoseBrowser } from './browser'; import type { ReportingRequestHandlerContext } from '../../types'; +import { registerDiagnoseBrowser } from './browser'; jest.mock('child_process'); jest.mock('readline'); @@ -38,25 +39,17 @@ describe('POST /diagnose/browser', () => { const mockedSpawn: any = spawn; const mockedCreateInterface: any = createInterface; - const config = { - get: jest.fn().mockImplementation((...keys) => { - const key = keys.join('.'); - switch (key) { - case 'queue.timeout': - return 120000; - case 'capture.browser.chromium.proxy': - return { enabled: false }; - } - }), - kbnConfig: { get: jest.fn() }, - }; + const config = createMockConfigSchema({ + queue: { timeout: 120000 }, + capture: { browser: { chromium: { proxy: { enabled: false } } } }, + }); beforeEach(async () => { ({ server, httpSetup } = await setupServer(reportingSymbol)); httpSetup.registerRouteHandlerContext( reportingSymbol, 'reporting', - () => ({}) + () => ({ usesUiCapabilities: () => false }) ); const mockSetupDeps = createMockPluginSetup({ diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts index 952a33ff64190..9e6a7769f6351 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts @@ -11,13 +11,16 @@ import { ElasticsearchClient } from 'kibana/server'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '../..'; +import { ReportingConfigType } from '../../config'; import { - createMockReportingCore, + createMockConfig, + createMockConfigSchema, createMockLevelLogger, createMockPluginSetup, + createMockReportingCore, } from '../../test_helpers'; -import { registerDiagnoseConfig } from './config'; import type { ReportingRequestHandlerContext } from '../../types'; +import { registerDiagnoseConfig } from './config'; type SetupServerReturn = UnwrapPromise>; @@ -27,7 +30,7 @@ describe('POST /diagnose/config', () => { let httpSetup: SetupServerReturn['httpSetup']; let core: ReportingCore; let mockSetupDeps: any; - let config: any; + let config: ReportingConfigType; let mockEsClient: DeeplyMockedKeys; const mockLogger = createMockLevelLogger(); @@ -37,26 +40,14 @@ describe('POST /diagnose/config', () => { httpSetup.registerRouteHandlerContext( reportingSymbol, 'reporting', - () => ({}) + () => ({ usesUiCapabilities: () => false }) ); mockSetupDeps = createMockPluginSetup({ router: httpSetup.createRouter(''), } as unknown) as any; - config = { - get: jest.fn().mockImplementation((...keys) => { - const key = keys.join('.'); - switch (key) { - case 'queue.timeout': - return 120000; - case 'csv.maxSizeBytes': - return 1024; - } - }), - kbnConfig: { get: jest.fn() }, - }; - + config = createMockConfigSchema({ queue: { timeout: 120000 }, csv: { maxSizeBytes: 1024 } }); core = await createMockReportingCore(config, mockSetupDeps); mockEsClient = (await core.getEsClient()).asInternalUser as typeof mockEsClient; }); @@ -94,7 +85,11 @@ describe('POST /diagnose/config', () => { }); it('returns a 200 with help text when not configured properly', async () => { - config.get.mockImplementation(() => 10485760); + core.setConfig( + createMockConfig( + createMockConfigSchema({ queue: { timeout: 120000 }, csv: { maxSizeBytes: 10485760 } }) + ) + ); mockEsClient.cluster.getSettings.mockResolvedValueOnce({ body: { defaults: { diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index 822dc6f5199a3..f151870004df7 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -13,6 +13,7 @@ import { createMockReportingCore, createMockLevelLogger, createMockPluginSetup, + createMockConfigSchema, } from '../../test_helpers'; import { registerDiagnoseScreenshot } from './screenshot'; import type { ReportingRequestHandlerContext } from '../../types'; @@ -38,14 +39,7 @@ describe('POST /diagnose/screenshot', () => { (generatePngObservableFactory as any).mockResolvedValue(generateMock); }; - const config = { - get: jest.fn().mockImplementation((...keys) => { - if (keys.join('.') === 'queue.timeout') { - return 120000; - } - }), - kbnConfig: { get: jest.fn() }, - }; + const config = createMockConfigSchema({ queue: { timeout: 120000 } }); const mockLogger = createMockLevelLogger(); beforeEach(async () => { @@ -53,7 +47,7 @@ describe('POST /diagnose/screenshot', () => { httpSetup.registerRouteHandlerContext( reportingSymbol, 'reporting', - () => ({}) + () => ({ usesUiCapabilities: () => false }) ); const mockSetupDeps = createMockPluginSetup({ diff --git a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts index 681d93f1f6dff..55d12e5c6d442 100644 --- a/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts +++ b/x-pack/plugins/reporting/server/routes/generate_from_jobparams.ts @@ -24,26 +24,22 @@ export function registerGenerateFromJobParams( const userHandler = authorizedUserPreRoutingFactory(reporting); const { router } = setupDeps; + // TODO: find a way to abstract this using ExportTypeRegistry: it needs a new + // public method to return this array + // const registry = reporting.getExportTypesRegistry(); + // const kibanaAccessControlTags = registry.getAllAccessControlTags(); + const useKibanaAccessControl = reporting.getDeprecatedAllowedRoles() === false; // true if Reporting's deprecated access control feature is disabled + const kibanaAccessControlTags = useKibanaAccessControl ? ['access:generateReport'] : []; + router.post( { path: `${BASE_GENERATE}/{exportType}`, validate: { - params: schema.object({ - exportType: schema.string({ minLength: 2 }), - }), - body: schema.nullable( - schema.object({ - jobParams: schema.maybe(schema.string()), - }) - ), - query: schema.nullable( - schema.object({ - jobParams: schema.string({ - defaultValue: '', - }), - }) - ), + params: schema.object({ exportType: schema.string({ minLength: 2 }) }), + body: schema.nullable(schema.object({ jobParams: schema.maybe(schema.string()) })), + query: schema.nullable(schema.object({ jobParams: schema.string({ defaultValue: '' }) })), }, + options: { tags: kibanaAccessControlTags }, }, userHandler(async (user, context, req, res) => { let jobParamsRison: null | string = null; diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index 0ce977e0a5431..c6889f3612b59 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -14,7 +14,10 @@ import supertest from 'supertest'; import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { createMockLevelLogger, createMockReportingCore } from '../test_helpers'; -import { createMockPluginSetup } from '../test_helpers/create_mock_reportingplugin'; +import { + createMockConfigSchema, + createMockPluginSetup, +} from '../test_helpers/create_mock_reportingplugin'; import { registerJobGenerationRoutes } from './generation'; import type { ReportingRequestHandlerContext } from '../types'; @@ -28,24 +31,15 @@ describe('POST /api/reporting/generate', () => { let core: ReportingCore; let mockEsClient: DeeplyMockedKeys; - const config = { - get: jest.fn().mockImplementation((...args) => { - const key = args.join('.'); - switch (key) { - case 'queue.indexInterval': - return 'year'; - case 'queue.timeout': - return 10000; - case 'index': - return '.reporting'; - case 'queue.pollEnabled': - return true; - default: - return; - } - }), - kbnConfig: { get: jest.fn() }, - }; + const config = createMockConfigSchema({ + queue: { + indexInterval: 'year', + timeout: 10000, + pollEnabled: true, + }, + index: '.reporting', + }); + const mockLogger = createMockLevelLogger(); beforeEach(async () => { @@ -53,7 +47,7 @@ describe('POST /api/reporting/generate', () => { httpSetup.registerRouteHandlerContext( reportingSymbol, 'reporting', - () => ({}) + () => ({ usesUiCapabilities: jest.fn() }) ); const mockSetupDeps = createMockPluginSetup({ diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts index 885fc701935fe..3f913dfd1f32f 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -15,7 +15,6 @@ import { ReportingCore } from '..'; import { ReportingInternalSetup } from '../core'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { - createMockConfig, createMockConfigSchema, createMockPluginSetup, createMockReportingCore, @@ -31,9 +30,9 @@ describe('GET /api/reporting/jobs/download', () => { let httpSetup: SetupServerReturn['httpSetup']; let exportTypesRegistry: ExportTypesRegistry; let core: ReportingCore; + let mockSetupDeps: ReportingInternalSetup; let mockEsClient: DeeplyMockedKeys; - const config = createMockConfig(createMockConfigSchema()); const getHits = (...sources: any) => { return { hits: { @@ -47,9 +46,9 @@ describe('GET /api/reporting/jobs/download', () => { httpSetup.registerRouteHandlerContext( reportingSymbol, 'reporting', - () => ({}) + () => ({ usesUiCapabilities: jest.fn() }) ); - const mockSetupDeps = createMockPluginSetup({ + mockSetupDeps = createMockPluginSetup({ security: { license: { isEnabled: () => true, @@ -72,7 +71,10 @@ describe('GET /api/reporting/jobs/download', () => { }, }); - core = await createMockReportingCore(config, mockSetupDeps); + core = await createMockReportingCore( + createMockConfigSchema({ roles: { enabled: false } }), + mockSetupDeps + ); // @ts-ignore exportTypesRegistry = new ExportTypesRegistry(); exportTypesRegistry.register({ @@ -139,36 +141,6 @@ describe('GET /api/reporting/jobs/download', () => { ); }); - it('fails on users without the appropriate role', async () => { - // @ts-ignore - core.pluginSetupDeps = ({ - // @ts-ignore - ...core.pluginSetupDeps, - security: { - license: { - isEnabled: () => true, - }, - authc: { - getCurrentUser: () => ({ - id: '123', - roles: ['peasant'], - username: 'Tom Riddle', - }), - }, - }, - } as unknown) as ReportingInternalSetup; - registerJobInfoRoutes(core); - - await server.start(); - - await supertest(httpSetup.server.listener) - .get('/api/reporting/jobs/download/dope') - .expect(403) - .then(({ body }) => - expect(body.message).toMatchInlineSnapshot(`"Sorry, you don't have access to Reporting"`) - ); - }); - it('returns 404 if job not found', async () => { mockEsClient.search.mockResolvedValueOnce({ body: getHits() } as any); registerJobInfoRoutes(core); @@ -329,4 +301,38 @@ describe('GET /api/reporting/jobs/download', () => { }); }); }); + + describe('Deprecated: role-based access control', () => { + it('fails on users without the appropriate role', async () => { + const deprecatedConfig = createMockConfigSchema({ roles: { enabled: true } }); + core = await createMockReportingCore(deprecatedConfig, mockSetupDeps); + // @ts-ignore + core.pluginSetupDeps = ({ + // @ts-ignore + ...core.pluginSetupDeps, + security: { + license: { + isEnabled: () => true, + }, + authc: { + getCurrentUser: () => ({ + id: '123', + roles: ['peasant'], + username: 'Tom Riddle', + }), + }, + }, + } as unknown) as ReportingInternalSetup; + registerJobInfoRoutes(core); + + await server.start(); + + await supertest(httpSetup.server.listener) + .get('/api/reporting/jobs/download/dope') + .expect(403) + .then(({ body }) => + expect(body.message).toMatchInlineSnapshot(`"Sorry, you don't have access to Reporting"`) + ); + }); + }); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 0f1bfa38ee6c8..16ef9e6d5bc10 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -5,22 +5,16 @@ * 2.0. */ -import { KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory } from 'src/core/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; import { ReportingInternalSetup } from '../../core'; -import { - createMockConfig, - createMockConfigSchema, - createMockReportingCore, -} from '../../test_helpers'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; +import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import type { ReportingRequestHandlerContext } from '../../types'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; let mockCore: ReportingCore; -const mockConfig: any = { 'server.basePath': '/sbp', 'roles.allow': ['reporting_user'] }; -const mockReportingConfigSchema = createMockConfigSchema(mockConfig); -const mockReportingConfig = createMockConfig(mockReportingConfigSchema); +const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); const getMockContext = () => (({ @@ -111,50 +105,64 @@ describe('authorized_user_pre_routing', function () { }); }); - it(`should return with 403 when security is enabled but user doesn't have the allowed role`, async function () { - mockCore.getPluginSetupDeps = () => - (({ - // @ts-ignore - ...mockCore.pluginSetupDeps, - security: { - license: { isEnabled: () => true }, - authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, + describe('Deprecated: security roles for access control', () => { + beforeEach(async () => { + const mockReportingConfigDeprecated = createMockConfigSchema({ + roles: { + allow: ['reporting_user'], + enabled: true, }, - } as unknown) as ReportingInternalSetup); - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); - const mockResponseFactory = getMockResponseFactory(); + }); + mockCore = await createMockReportingCore(mockReportingConfigDeprecated); + }); - const mockHandler = () => { - throw new Error('Handler callback should not be called'); - }; - expect( - authorizedUserPreRouting(mockHandler)(getMockContext(), getMockRequest(), mockResponseFactory) - ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); - }); + it(`should return with 403 when security is enabled but user doesn't have the allowed role`, async function () { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + license: { isEnabled: () => true }, + authc: { getCurrentUser: () => ({ username: 'friendlyuser', roles: ['cowboy'] }) }, + }, + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + const mockHandler = () => { + throw new Error('Handler callback should not be called'); + }; + expect( + authorizedUserPreRouting(mockHandler)( + getMockContext(), + getMockRequest(), + mockResponseFactory + ) + ).toMatchObject({ body: `Sorry, you don't have access to Reporting` }); + }); - it('should return from handler when security is enabled and user has explicitly allowed role', function (done) { - mockCore.getPluginSetupDeps = () => - (({ - // @ts-ignore - ...mockCore.pluginSetupDeps, - security: { - license: { isEnabled: () => true }, - authc: { - getCurrentUser: () => ({ username: 'friendlyuser', roles: ['reporting_user'] }), + it('should return from handler when security is enabled and user has explicitly allowed role', function (done) { + mockCore.getPluginSetupDeps = () => + (({ + // @ts-ignore + ...mockCore.pluginSetupDeps, + security: { + license: { isEnabled: () => true }, + authc: { + getCurrentUser: () => ({ username: 'friendlyuser', roles: ['reporting_user'] }), + }, }, - }, - } as unknown) as ReportingInternalSetup); - // @ts-ignore overloading config getter - mockCore.config = mockReportingConfig; - const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); - const mockResponseFactory = getMockResponseFactory(); + } as unknown) as ReportingInternalSetup); + const authorizedUserPreRouting = authorizedUserPreRoutingFactory(mockCore); + const mockResponseFactory = getMockResponseFactory(); + + authorizedUserPreRouting((user) => { + expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); + done(); + return Promise.resolve({ status: 200, options: {} }); + })(getMockContext(), getMockRequest(), mockResponseFactory); + }); - authorizedUserPreRouting((user) => { - expect(user).toMatchObject({ roles: ['reporting_user'], username: 'friendlyuser' }); - done(); - return Promise.resolve({ status: 200, options: {} }); - })(getMockContext(), getMockRequest(), mockResponseFactory); + it('should return from handler when security is enabled and user has superuser role', async function () {}); }); - - it('should return from handler when security is enabled and user has superuser role', async function () {}); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts index d2576224fc792..846d8c28a5378 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.ts @@ -26,35 +26,40 @@ export type RequestHandlerUser = RequestHandler< export const authorizedUserPreRoutingFactory = function authorizedUserPreRoutingFn( reporting: ReportingCore ) { - const setupDeps = reporting.getPluginSetupDeps(); - const getUser = getUserFactory(setupDeps.security); + const { logger, security } = reporting.getPluginSetupDeps(); + const getUser = getUserFactory(security); return ( handler: RequestHandlerUser ): RequestHandler => { return (context, req, res) => { - let user: ReportingRequestUser = false; - if (setupDeps.security && setupDeps.security.license.isEnabled()) { - // find the authenticated user, or null if security is not enabled - user = getUser(req); - if (!user) { - // security is enabled but the user is null - return res.unauthorized({ body: `Sorry, you aren't authenticated` }); + try { + let user: ReportingRequestUser = false; + if (security && security.license.isEnabled()) { + // find the authenticated user, or null if security is not enabled + user = getUser(req); + if (!user) { + // security is enabled but the user is null + return res.unauthorized({ body: `Sorry, you aren't authenticated` }); + } } - } - if (user) { - // check allowance with the configured set of roleas + "superuser" - const config = reporting.getConfig(); - const allowedRoles = config.get('roles', 'allow') || []; - const authorizedRoles = [superuserRole, ...allowedRoles]; + const deprecatedAllowedRoles = reporting.getDeprecatedAllowedRoles(); + if (user && deprecatedAllowedRoles !== false) { + // check allowance with the configured set of roleas + "superuser" + const allowedRoles = deprecatedAllowedRoles || []; + const authorizedRoles = [superuserRole, ...allowedRoles]; - if (!user.roles.find((role) => authorizedRoles.includes(role))) { - // user's roles do not allow - return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); + if (!user.roles.find((role) => authorizedRoles.includes(role))) { + // user's roles do not allow + return res.forbidden({ body: `Sorry, you don't have access to Reporting` }); + } } - } - return handler(user, context, req, res); + return handler(user, context, req, res); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } }; }; }; diff --git a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts index cbdb39f7a935e..8ffefa9c8a98c 100644 --- a/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts +++ b/x-pack/plugins/reporting/server/routes/lib/job_response_handler.ts @@ -32,37 +32,42 @@ export function downloadJobResponseHandlerFactory(reporting: ReportingCore) { params: JobResponseHandlerParams, opts: JobResponseHandlerOpts = {} ) { - const { docId } = params; - - const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }); - if (!doc) { - return res.notFound(); - } - - const { jobtype: jobType } = doc._source; - - if (!validJobTypes.includes(jobType)) { - return res.unauthorized({ - body: `Sorry, you are not authorized to download ${jobType} reports`, - }); - } - - const payload = getDocumentPayload(doc); - - if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { - return res.badRequest({ - body: `Unsupported content-type of ${payload.contentType} specified by job output`, + try { + const { docId } = params; + + const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent }); + if (!doc) { + return res.notFound(); + } + + const { jobtype: jobType } = doc._source; + + if (!validJobTypes.includes(jobType)) { + return res.unauthorized({ + body: `Sorry, you are not authorized to download ${jobType} reports`, + }); + } + + const payload = getDocumentPayload(doc); + + if (!payload.contentType || !ALLOWED_JOB_CONTENT_TYPES.includes(payload.contentType)) { + return res.badRequest({ + body: `Unsupported content-type of ${payload.contentType} specified by job output`, + }); + } + + return res.custom({ + body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, + statusCode: payload.statusCode, + headers: { + ...payload.headers, + 'content-type': payload.contentType || '', + }, }); + } catch (err) { + const { logger } = reporting.getPluginSetupDeps(); + logger.error(err); } - - return res.custom({ - body: typeof payload.content === 'string' ? Buffer.from(payload.content) : payload.content, - statusCode: payload.statusCode, - headers: { - ...payload.headers, - 'content-type': payload.contentType || '', - }, - }); }; } diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index bd45aa2ab6684..5caf9b798ad1e 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -42,6 +42,7 @@ export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup = router: setupMock.router, security: setupMock.security, licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any, + logger: createMockLevelLogger(), ...setupMock, }; }; @@ -66,6 +67,7 @@ export const createMockPluginStart = ( uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, data: startMock.data || dataPluginMock.createStartContract(), store, + logger: createMockLevelLogger(), ...startMock, }; }; @@ -76,6 +78,7 @@ interface ReportingConfigTestType { queue: Partial; kibanaServer: Partial; csv: Partial; + roles?: Partial; capture: any; server?: any; } @@ -111,6 +114,10 @@ export const createMockConfigSchema = ( csv: { ...overrides.csv, }, + roles: { + enabled: false, + ...overrides.roles, + }, } as any; }; @@ -127,12 +134,12 @@ export const createMockConfig = ( }; export const createMockReportingCore = async ( - config: ReportingConfig, + config: ReportingConfigType, setupDepsMock: ReportingInternalSetup | undefined = undefined, startDepsMock: ReportingInternalStart | undefined = undefined ) => { const mockReportingCore = ({ - getConfig: () => config, + getConfig: () => createMockConfig(config), getEsClient: () => startDepsMock?.esClient, getDataService: () => startDepsMock?.data, } as unknown) as ReportingCore; @@ -145,8 +152,10 @@ export const createMockReportingCore = async ( } const context = coreMock.createPluginInitializerContext(createMockConfigSchema()); - const core = new ReportingCore(logger); - core.setConfig(config); + context.config = { get: () => config } as any; + + const core = new ReportingCore(logger, context); + core.setConfig(createMockConfig(config)); core.pluginSetup(setupDepsMock); await core.pluginSetsUp(); diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index f84d2fbdf4712..7ee3403a606a8 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -36,8 +36,11 @@ export interface ReportingStartDeps { data: DataPluginStart; } -export type ReportingStart = object; -export type ReportingSetup = object; +export interface ReportingSetup { + usesUiCapabilities: () => boolean; +} + +export type ReportingStart = ReportingSetup; /* * Internal Types @@ -97,8 +100,9 @@ export interface ExportTypeDefinition< /** * @internal */ -export interface ReportingRequestHandlerContext extends RequestHandlerContext { +export interface ReportingRequestHandlerContext { reporting: ReportingStart | null; + core: RequestHandlerContext['core']; } /** diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index 05b80bc8acc75..226704b255ab3 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -9,9 +9,9 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; import { CollectorFetchContext, UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; -import { ReportingConfig, ReportingCore } from '../'; +import { ReportingCore } from '../'; import { getExportTypesRegistry } from '../lib/export_types_registry'; -import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; +import { createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ReportingSetupDeps } from '../types'; import { FeaturesAvailability } from './'; import { @@ -64,11 +64,9 @@ const getMockFetchClients = (resp: any) => { return fetchParamsMock; }; describe('license checks', () => { - let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = createMockConfig(createMockConfigSchema()); - mockCore = await createMockReportingCore(mockConfig); + mockCore = await createMockReportingCore(createMockConfigSchema()); }); describe('with a basic license', () => { @@ -185,12 +183,10 @@ describe('license checks', () => { }); describe('data modeling', () => { - let mockConfig: ReportingConfig; let mockCore: ReportingCore; let collectorFetchContext: CollectorFetchContext; beforeAll(async () => { - mockConfig = createMockConfig(createMockConfigSchema()); - mockCore = await createMockReportingCore(mockConfig); + mockCore = await createMockReportingCore(createMockConfigSchema()); }); test('with normal looking usage data', async () => { const plugins = getPluginsMock(); @@ -456,8 +452,7 @@ describe('data modeling', () => { describe('Ready for collection observable', () => { test('converts observable to promise', async () => { - const mockConfig = createMockConfig(createMockConfigSchema()); - const mockReporting = await createMockReportingCore(mockConfig); + const mockReporting = await createMockReportingCore(createMockConfigSchema()); const usageCollection = getMockUsageCollection(); const makeCollectorSpy = sinon.spy(); diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 269e6f0c44a0a..11ae755096f0b 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -16,6 +16,7 @@ require('@kbn/test').runTestsCli([ require.resolve('../test/functional/config_security_basic.ts'), require.resolve('../test/reporting_functional/reporting_and_security.config.ts'), require.resolve('../test/reporting_functional/reporting_without_security.config.ts'), + require.resolve('../test/reporting_functional/reporting_and_deprecated_security.config.ts'), require.resolve('../test/security_functional/login_selector.config.ts'), require.resolve('../test/security_functional/oidc.config.ts'), require.resolve('../test/security_functional/saml.config.ts'), diff --git a/x-pack/test/api_integration/apis/security/license_downgrade.ts b/x-pack/test/api_integration/apis/security/license_downgrade.ts index 3017bd005b776..583df6ea5ed07 100644 --- a/x-pack/test/api_integration/apis/security/license_downgrade.ts +++ b/x-pack/test/api_integration/apis/security/license_downgrade.ts @@ -26,6 +26,7 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', 'url_create', 'store_search_session', + 'generate_report', ]; const trialPrivileges = await supertest .get('/api/security/privileges') diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 9df7eddfd0025..f08712e015656 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -29,8 +29,16 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', 'url_create', 'store_search_session', + 'generate_report', + ], + visualize: [ + 'all', + 'read', + 'minimal_all', + 'minimal_read', + 'url_create', + 'generate_report', ], - visualize: ['all', 'read', 'minimal_all', 'minimal_read', 'url_create'], dashboard: [ 'all', 'read', @@ -38,6 +46,8 @@ export default function ({ getService }: FtrProviderContext) { 'minimal_read', 'url_create', 'store_search_session', + 'generate_report', + 'download_csv_report', ], dev_tools: ['all', 'read'], advancedSettings: ['all', 'read'], @@ -47,7 +57,7 @@ export default function ({ getService }: FtrProviderContext) { timelion: ['all', 'read'], graph: ['all', 'read'], maps: ['all', 'read'], - canvas: ['all', 'read'], + canvas: ['all', 'read', 'minimal_all', 'minimal_read', 'generate_report'], infrastructure: ['all', 'read'], logs: ['all', 'read'], uptime: ['all', 'read'], diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index b21fba54f1f1a..7f5f5d09f28db 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { kibana: [ { feature: { - canvas: ['all'], + canvas: ['minimal_all'], }, spaces: ['*'], }, diff --git a/x-pack/test/functional/apps/canvas/reports.ts b/x-pack/test/functional/apps/canvas/reports.ts index 4116a46fe51ae..7edbca783d928 100644 --- a/x-pack/test/functional/apps/canvas/reports.ts +++ b/x-pack/test/functional/apps/canvas/reports.ts @@ -20,7 +20,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Canvas PDF Report Generation', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); - await security.testUser.setRoles(['kibana_admin', 'reporting_user']); + await security.role.create('test_reporting_user', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { canvas: ['minimal_read', 'generate_report'] }, + }, + ], + }); + await security.testUser.setRoles(['kibana_admin', 'test_reporting_user']); await esArchiver.load('canvas/reports'); await browser.setWindowSize(1600, 850); }); diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts index b16dc828e1776..a24b18490be74 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts @@ -20,6 +20,7 @@ const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); const esArchiver = getService('esArchiver'); + const security = getService('security'); const browser = getService('browser'); const log = getService('log'); const config = getService('config'); @@ -29,10 +30,32 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { describe('Dashboard Reporting Screenshots', () => { before('initialize tests', async () => { - log.debug('ReportingPage:initTests'); await esArchiver.loadIfNeeded('reporting/ecommerce'); await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); await browser.setWindowSize(1600, 850); + + await security.role.create('test_reporting_user', { + elasticsearch: { + cluster: [], + indices: [ + { + names: ['ecommerce'], + privileges: ['read'], + field_security: { grant: ['*'], except: [] }, + }, + ], + run_as: [], + }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { dashboard: ['minimal_all', 'generate_report'] }, + }, + ], + }); + + await security.testUser.setRoles(['test_reporting_user']); }); after('clean up archives', async () => { await esArchiver.unload('reporting/ecommerce'); @@ -42,6 +65,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { refresh: true, body: { query: { match_all: {} } }, }); + await security.testUser.restoreDefaults(); }); describe('Print PDF button', () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index d595dc98a9a1a..f44d7c42a23c1 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -75,6 +75,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expectSpaceSelector: false, } ); + await PageObjects.common.navigateToApp('discover'); }); after(async () => { @@ -87,12 +88,11 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks.map((link) => link.text)).to.eql([ 'Overview', 'Discover', - 'Stack Management', // because `global_discover_all_role` enables search sessions + 'Stack Management', // because `global_discover_all_role` enables search sessions and reporting ]); }); it('shows save button', async () => { - await PageObjects.common.navigateToApp('discover'); await testSubjects.existOrFail('discoverSaveButton', { timeout: 20000 }); }); @@ -107,6 +107,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.share.clickShareTopNavButton(); }); + it('shows CSV reports', async () => { + await PageObjects.share.clickShareTopNavButton(); + await testSubjects.existOrFail('sharePanel-CSVReports'); + await PageObjects.share.clickShareTopNavButton(); + }); + it('allows saving via the saved query management component popover with no saved query loaded', async () => { await queryBar.setQuery('response:200'); await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false); @@ -213,8 +219,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`Permalinks doesn't show create short-url button`, async () => { - await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.clickShareTopNavButton(); await PageObjects.share.createShortUrlMissingOrFail(); + await PageObjects.share.clickShareTopNavButton(); + }); + + it(`doesn't show CSV reports`, async () => { + await PageObjects.share.clickShareTopNavButton(); + await testSubjects.missingOrFail('sharePanel-CSVReports'); + await PageObjects.share.clickShareTopNavButton(); }); it('allows loading a saved query via the saved query management component', async () => { @@ -304,7 +317,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('Permalinks shows create short-url button', async () => { - await PageObjects.share.openShareMenuItem('Permalinks'); + await PageObjects.share.clickShareTopNavButton(); await PageObjects.share.createShortUrlExistOrFail(); // close the menu await PageObjects.share.clickShareTopNavButton(); diff --git a/x-pack/test/functional/apps/lens/lens_reporting.ts b/x-pack/test/functional/apps/lens/lens_reporting.ts index e8f1916a3630c..658a9dbcac822 100644 --- a/x-pack/test/functional/apps/lens/lens_reporting.ts +++ b/x-pack/test/functional/apps/lens/lens_reporting.ts @@ -18,8 +18,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('lens reporting', () => { before(async () => { await esArchiver.loadIfNeeded('lens/reporting'); + await security.role.create('test_reporting_user', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { dashboard: ['minimal_read', 'generate_report'] }, + }, + ], + }); await security.testUser.setRoles( - ['test_logstash_reader', 'global_dashboard_read', 'reporting_user'], + ['test_logstash_reader', 'global_dashboard_read', 'test_reporting_user'], false ); }); diff --git a/x-pack/test/functional/apps/management/feature_controls/management_security.ts b/x-pack/test/functional/apps/management/feature_controls/management_security.ts index 7d121e9100749..24d3455219fe5 100644 --- a/x-pack/test/functional/apps/management/feature_controls/management_security.ts +++ b/x-pack/test/functional/apps/management/feature_controls/management_security.ts @@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(sections).to.have.length(2); expect(sections[0]).to.eql({ sectionId: 'insightsAndAlerting', - sectionLinks: ['triggersActions'], + sectionLinks: ['triggersActions', 'reporting'], }); expect(sections[1]).to.eql({ sectionId: 'kibana', diff --git a/x-pack/test/functional/apps/reporting_management/report_listing.ts b/x-pack/test/functional/apps/reporting_management/report_listing.ts index 964e6485aff0b..e6503b1550001 100644 --- a/x-pack/test/functional/apps/reporting_management/report_listing.ts +++ b/x-pack/test/functional/apps/reporting_management/report_listing.ts @@ -19,7 +19,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('Listing of Reports', function () { before(async () => { - await security.testUser.setRoles(['kibana_admin', 'reporting_user']); + await security.role.create('test_reporting_user', { + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [ + { + spaces: ['*'], + base: [], + feature: { canvas: ['minimal_read', 'generate_report'] }, + }, + ], + }); + await security.testUser.setRoles(['kibana_admin', 'test_reporting_user']); await esArchiver.load('empty_kibana'); }); diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 95ebc7b2ff5d5..d9ba3a78eff13 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -262,7 +262,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { kibana: [ { feature: { - visualize: ['all'], + visualize: ['minimal_all'], }, spaces: ['*'], }, 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 a1f258714bb0d..33913bcbbf7f0 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }) { 'monitoring', 'discover', 'common', - 'reporting', + 'share', 'header', ]); const log = getService('log'); @@ -59,13 +59,13 @@ export default function ({ getService, getPageObjects }) { confirm_password: 'changeme', full_name: 'RashmiFirst RashmiLast', email: 'rashmi@myEmail.com', - roles: ['logstash_reader', 'kibana_admin'], + roles: ['logstash_reader'], }); log.debug('After Add user: , userObj.userName'); const users = keyBy(await PageObjects.security.getElasticsearchUsers(), 'username'); log.debug('actualUsers = %j', users); log.debug('roles: ', users.Rashmi.roles); - expect(users.Rashmi.roles).to.eql(['logstash_reader', 'kibana_admin']); + expect(users.Rashmi.roles).to.eql(['logstash_reader']); expect(users.Rashmi.fullname).to.eql('RashmiFirst RashmiLast'); expect(users.Rashmi.reserved).to.be(false); await PageObjects.security.forceLogout(); @@ -77,14 +77,12 @@ export default function ({ getService, getPageObjects }) { await testSubjects.missingOrFail('users'); }); - it('Kibana User navigating to Discover and trying to generate CSV gets - Authorization Error ', async function () { + it('Kibana User navigating to Discover sees the generate CSV button', async function () { await PageObjects.common.navigateToApp('discover'); await PageObjects.discover.loadSavedSearch('A Saved Search'); - log.debug('click Reporting button'); - await PageObjects.reporting.openCsvReportingPanel(); - await PageObjects.reporting.clickGenerateReportButton(); - const queueReportError = await PageObjects.reporting.getQueueReportError(); - expect(queueReportError).to.be(true); + log.debug('click Top Nav Share button'); + await PageObjects.share.clickShareTopNavButton(); + await testSubjects.existOrFail('sharePanel-CSVReports'); }); after(async function () { diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index d6644cee21198..f650ac08de166 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -81,7 +81,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Visualize Library']); + expect(navLinks).to.eql(['Overview', 'Visualize Library', 'Stack Management']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 9cb7e2e721133..eb77af8c5bef6 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -84,6 +84,7 @@ export default async function ({ readConfigFile }) { '--xpack.maps.showMapsInspectorAdapter=true', '--xpack.maps.preserveDrawingBuffer=true', '--xpack.maps.enableDrawingFeature=true', + '--xpack.reporting.roles.enabled=false', // use the non-deprecated access control model for Reporting '--xpack.reporting.queue.pollInterval=3000', // make it explicitly the default '--xpack.reporting.csv.maxSizeBytes=2850', // small-ish limit for cutting off a 1999 byte report '--usageCollection.maximumWaitTimeForAllCollectorsInS=1', @@ -236,8 +237,8 @@ export default async function ({ readConfigFile }) { kibana: [ { feature: { - canvas: ['all'], - visualize: ['all'], + canvas: ['minimal_all'], + visualize: ['minimal_all'], }, spaces: ['*'], }, diff --git a/x-pack/test/licensing_plugin/public/updates.ts b/x-pack/test/licensing_plugin/public/updates.ts index e09eb04065b64..d7442e491875a 100644 --- a/x-pack/test/licensing_plugin/public/updates.ts +++ b/x-pack/test/licensing_plugin/public/updates.ts @@ -19,7 +19,8 @@ export default function (ftrContext: FtrProviderContext) { const scenario = createScenario(ftrContext); - describe('changes in license types', () => { + // FLAKY: https://github.com/elastic/kibana/issues/53575 + describe.skip('changes in license types', () => { after(async () => { await scenario.teardown(); }); @@ -34,7 +35,7 @@ export default function (ftrContext: FtrProviderContext) { // this call enforces signature check to detect license update // and causes license re-fetch await setup.core.http.get('/'); - await testUtils.delay(500); + await testUtils.delay(1000); const licensing: LicensingPluginSetup = setup.plugins.licensing; licensing.license$.subscribe((license) => cb(license.type)); @@ -50,7 +51,7 @@ export default function (ftrContext: FtrProviderContext) { // this call enforces signature check to detect license update // and causes license re-fetch await setup.core.http.get('/'); - await testUtils.delay(500); + await testUtils.delay(1000); const licensing: LicensingPluginSetup = setup.plugins.licensing; licensing.license$.subscribe((license) => cb(license.type)); @@ -66,7 +67,7 @@ export default function (ftrContext: FtrProviderContext) { // this call enforces signature check to detect license update // and causes license re-fetch await setup.core.http.get('/'); - await testUtils.delay(500); + await testUtils.delay(1000); const licensing: LicensingPluginSetup = setup.plugins.licensing; licensing.license$.subscribe((license) => cb(license.type)); @@ -82,7 +83,7 @@ export default function (ftrContext: FtrProviderContext) { // this call enforces signature check to detect license update // and causes license re-fetch await setup.core.http.get('/'); - await testUtils.delay(500); + await testUtils.delay(1000); const licensing: LicensingPluginSetup = setup.plugins.licensing; licensing.license$.subscribe((license) => cb(license.type)); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts index ef1bd03d805df..e33efff0c6bf1 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/index.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/index.ts @@ -15,6 +15,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { before(async () => { const reportingAPI = getService('reportingAPI'); await reportingAPI.createDataAnalystRole(); + await reportingAPI.createTestReportingUserRole(); await reportingAPI.createDataAnalyst(); await reportingAPI.createTestReportingUser(); }); diff --git a/x-pack/test/reporting_api_integration/services/scenarios.ts b/x-pack/test/reporting_api_integration/services/scenarios.ts index d13deac3578ba..eee13b0bf07a2 100644 --- a/x-pack/test/reporting_api_integration/services/scenarios.ts +++ b/x-pack/test/reporting_api_integration/services/scenarios.ts @@ -58,6 +58,35 @@ export function createScenarios({ getService }: Pick { + await security.role.create('test_reporting_user', { + metadata: {}, + elasticsearch: { + cluster: [], + indices: [ + { + names: ['ecommerce'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + }, + ], + run_as: [], + }, + kibana: [ + { + base: [], + feature: { + dashboard: ['minimal_read', 'download_csv_report', 'generate_report'], + discover: ['minimal_read', 'generate_report'], + canvas: ['minimal_read', 'generate_report'], + visualize: ['minimal_read', 'generate_report'], + }, + spaces: ['*'], + }, + ], + }); + }; + const createDataAnalyst = async () => { await security.user.create('data_analyst', { password: 'data_analyst-password', @@ -69,7 +98,7 @@ export function createScenarios({ getService }: Pick { await security.user.create('reporting_user', { password: 'reporting_user-password', - roles: ['data_analyst', 'reporting_user'], + roles: ['test_reporting_user'], full_name: 'Reporting User', }); }; @@ -142,6 +171,7 @@ export function createScenarios({ getService }: Pick { + await security.role.create('data_analyst', { + metadata: {}, + elasticsearch: { + cluster: [], + indices: [ + { + names: ['ecommerce'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + }, + ], + run_as: [], + }, + kibana: [{ base: ['all'], feature: {}, spaces: ['*'] }], + }); + }; + const createDataAnalyst = async () => { + await security.user.create('data_analyst', { + password: 'data_analyst-password', + roles: ['data_analyst', 'kibana_user'], + full_name: 'a kibana user called data_a', + }); + }; + const createReportingUser = async () => { + await security.user.create('reporting_user', { + password: 'reporting_user-password', + roles: ['reporting_user', 'data_analyst', 'kibana_user'], // Deprecated: using built-in `reporting_user` role grants all Reporting privileges + full_name: 'a reporting user', + }); + }; + + describe('Reporting Functional Tests with Deprecated Security configuration enabled', function () { + this.tags('ciGroup2'); + + before(async () => { + await createDataAnalystRole(); + await createDataAnalyst(); + await createReportingUser(); + }); + + const { loadTestFile } = context; + loadTestFile(require.resolve('./security_roles_privileges')); + loadTestFile(require.resolve('./management')); + }); +} diff --git a/x-pack/test/reporting_functional/reporting_and_deprecated_security/management.ts b/x-pack/test/reporting_functional/reporting_and_deprecated_security/management.ts new file mode 100644 index 0000000000000..dba16c798d4ff --- /dev/null +++ b/x-pack/test/reporting_functional/reporting_and_deprecated_security/management.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService, getPageObjects }: FtrProviderContext) => { + const PageObjects = getPageObjects(['common', 'reporting', 'discover']); + + const testSubjects = getService('testSubjects'); + const reportingFunctional = getService('reportingFunctional'); + + describe('Access to Management > Reporting', () => { + before(async () => { + await reportingFunctional.initEcommerce(); + }); + after(async () => { + await reportingFunctional.teardownEcommerce(); + }); + + it('does not allow user that does not have reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await PageObjects.common.navigateToApp('reporting'); + await testSubjects.missingOrFail('reportJobListing'); + }); + + it('does allow user with reporting_user role', async () => { + await reportingFunctional.loginReportingUser(); + await PageObjects.common.navigateToApp('reporting'); + await testSubjects.existOrFail('reportJobListing'); + }); + }); +}; diff --git a/x-pack/test/reporting_functional/reporting_and_deprecated_security/security_roles_privileges.ts b/x-pack/test/reporting_functional/reporting_and_deprecated_security/security_roles_privileges.ts new file mode 100644 index 0000000000000..76ccb01477856 --- /dev/null +++ b/x-pack/test/reporting_functional/reporting_and_deprecated_security/security_roles_privileges.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +const DASHBOARD_TITLE = 'Ecom Dashboard'; +const SAVEDSEARCH_TITLE = 'Ecommerce Data'; +const VIS_TITLE = 'e-commerce pie chart'; +const CANVAS_TITLE = 'The Very Cool Workpad for PDF Tests'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const reportingFunctional = getService('reportingFunctional'); + + describe('Security with `reporting_user` built-in role', () => { + before(async () => { + await reportingFunctional.initEcommerce(); + }); + after(async () => { + await reportingFunctional.teardownEcommerce(); + }); + + describe('Dashboard: Download CSV file', () => { + it('does not allow user that does not have reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); + await reportingFunctional.tryDashboardDownloadCsvFail('Ecommerce Data'); + }); + + it('does allow user with reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); + await reportingFunctional.tryDashboardDownloadCsvSuccess('Ecommerce Data'); + }); + }); + + describe('Dashboard: Generate Screenshot', () => { + it('does not allow user that does not have reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); + await reportingFunctional.tryGeneratePdfFail(); + }); + + it('does allow user with reporting_user role', async () => { + await reportingFunctional.loginReportingUser(); + await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); + await reportingFunctional.tryGeneratePdfSuccess(); + }); + }); + + describe('Discover: Generate CSV', () => { + it('does not allow user that does not have reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await reportingFunctional.openSavedSearch(SAVEDSEARCH_TITLE); + await reportingFunctional.tryDiscoverCsvFail(); + }); + + it('does allow user with reporting_user role', async () => { + await reportingFunctional.loginReportingUser(); + await reportingFunctional.openSavedSearch(SAVEDSEARCH_TITLE); + await reportingFunctional.tryDiscoverCsvSuccess(); + }); + }); + + describe('Canvas: Generate PDF', () => { + const esArchiver = getService('esArchiver'); + const reportingApi = getService('reportingAPI'); + before('initialize tests', async () => { + await esArchiver.load('canvas/reports'); + }); + + after('teardown tests', async () => { + await esArchiver.unload('canvas/reports'); + await reportingApi.deleteAllReports(); + await reportingFunctional.initEcommerce(); + }); + + it('does not allow user that does not have reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await reportingFunctional.openCanvasWorkpad(CANVAS_TITLE); + await reportingFunctional.tryGeneratePdfFail(); + }); + + it('does allow user with reporting_user role', async () => { + await reportingFunctional.loginReportingUser(); + await reportingFunctional.openCanvasWorkpad(CANVAS_TITLE); + await reportingFunctional.tryGeneratePdfSuccess(); + }); + }); + + describe('Visualize Editor: Generate Screenshot', () => { + it('does not allow user that does not have reporting_user role', async () => { + await reportingFunctional.loginDataAnalyst(); + await reportingFunctional.openSavedVisualization(VIS_TITLE); + await reportingFunctional.tryGeneratePdfFail(); + }); + + it('does allow user with reporting_user role', async () => { + await reportingFunctional.loginReportingUser(); + await reportingFunctional.openSavedVisualization(VIS_TITLE); + await reportingFunctional.tryGeneratePdfSuccess(); + }); + }); + }); +} diff --git a/x-pack/test/reporting_functional/reporting_and_security/index.ts b/x-pack/test/reporting_functional/reporting_and_security/index.ts index f3e01453b0a59..be0e76a28bd0b 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/index.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/index.ts @@ -9,46 +9,15 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default function ({ getService, loadTestFile }: FtrProviderContext) { - const security = getService('security'); - const createDataAnalystRole = async () => { - await security.role.create('data_analyst', { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [ - { - names: ['ecommerce'], - privileges: ['read', 'view_index_metadata'], - allow_restricted_indices: false, - }, - ], - run_as: [], - }, - kibana: [{ base: ['all'], feature: {}, spaces: ['*'] }], - }); - }; - const createDataAnalyst = async () => { - await security.user.create('data_analyst', { - password: 'data_analyst-password', - roles: ['data_analyst', 'kibana_user'], - full_name: 'a kibana user called data_a', - }); - }; - const createReportingUser = async () => { - await security.user.create('reporting_user', { - password: 'reporting_user-password', - roles: ['reporting_user', 'data_analyst', 'kibana_user'], - full_name: 'a reporting user', - }); - }; - - describe('Reporting Functional Tests with Role-based Security configuration enabled', function () { + describe('Reporting Functional Tests with Security enabled', function () { this.tags('ciGroup2'); before(async () => { - await createDataAnalystRole(); - await createDataAnalyst(); - await createReportingUser(); + const reportingFunctional = getService('reportingFunctional'); + await reportingFunctional.createDataAnalystRole(); + await reportingFunctional.createDataAnalyst(); + await reportingFunctional.createTestReportingUserRole(); + await reportingFunctional.createTestReportingUser(); }); loadTestFile(require.resolve('./security_roles_privileges')); diff --git a/x-pack/test/reporting_functional/reporting_and_security/management.ts b/x-pack/test/reporting_functional/reporting_and_security/management.ts index dba16c798d4ff..304c175f0cb5d 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/management.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/management.ts @@ -9,8 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService, getPageObjects }: FtrProviderContext) => { - const PageObjects = getPageObjects(['common', 'reporting', 'discover']); - + const PageObjects = getPageObjects(['common']); const testSubjects = getService('testSubjects'); const reportingFunctional = getService('reportingFunctional'); @@ -22,13 +21,13 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { await reportingFunctional.teardownEcommerce(); }); - it('does not allow user that does not have reporting_user role', async () => { + it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); await PageObjects.common.navigateToApp('reporting'); await testSubjects.missingOrFail('reportJobListing'); }); - it('does allow user with reporting_user role', async () => { + it('does allow user with reporting privileges', async () => { await reportingFunctional.loginReportingUser(); await PageObjects.common.navigateToApp('reporting'); await testSubjects.existOrFail('reportJobListing'); diff --git a/x-pack/test/reporting_functional/reporting_and_security/security_roles_privileges.ts b/x-pack/test/reporting_functional/reporting_and_security/security_roles_privileges.ts index 76ccb01477856..20b88b22b542c 100644 --- a/x-pack/test/reporting_functional/reporting_and_security/security_roles_privileges.ts +++ b/x-pack/test/reporting_functional/reporting_and_security/security_roles_privileges.ts @@ -25,41 +25,47 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Dashboard: Download CSV file', () => { - it('does not allow user that does not have reporting_user role', async () => { + it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); - await reportingFunctional.tryDashboardDownloadCsvFail('Ecommerce Data'); + await reportingFunctional.tryDashboardDownloadCsvNotAvailable('Ecommerce Data'); }); - it('does allow user with reporting_user role', async () => { - await reportingFunctional.loginDataAnalyst(); + it('does allow user with reporting privileges', async () => { + await reportingFunctional.loginReportingUser(); await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); await reportingFunctional.tryDashboardDownloadCsvSuccess('Ecommerce Data'); }); }); describe('Dashboard: Generate Screenshot', () => { - it('does not allow user that does not have reporting_user role', async () => { + it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); - await reportingFunctional.tryGeneratePdfFail(); + await reportingFunctional.tryReportsNotAvailable(); }); - it('does allow user with reporting_user role', async () => { + it('does allow PDF generation user with reporting privileges', async () => { await reportingFunctional.loginReportingUser(); await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); await reportingFunctional.tryGeneratePdfSuccess(); }); + + it('does allow PNG generation user with reporting privileges', async () => { + await reportingFunctional.loginReportingUser(); + await reportingFunctional.openSavedDashboard(DASHBOARD_TITLE); + await reportingFunctional.tryGeneratePngSuccess(); + }); }); describe('Discover: Generate CSV', () => { - it('does not allow user that does not have reporting_user role', async () => { + it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); await reportingFunctional.openSavedSearch(SAVEDSEARCH_TITLE); - await reportingFunctional.tryDiscoverCsvFail(); + await reportingFunctional.tryDiscoverCsvNotAvailable(); }); - it('does allow user with reporting_user role', async () => { + it('does allow user with reporting privileges', async () => { await reportingFunctional.loginReportingUser(); await reportingFunctional.openSavedSearch(SAVEDSEARCH_TITLE); await reportingFunctional.tryDiscoverCsvSuccess(); @@ -79,13 +85,13 @@ export default function ({ getService }: FtrProviderContext) { await reportingFunctional.initEcommerce(); }); - it('does not allow user that does not have reporting_user role', async () => { + it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); await reportingFunctional.openCanvasWorkpad(CANVAS_TITLE); - await reportingFunctional.tryGeneratePdfFail(); + await reportingFunctional.tryGeneratePdfNotAvailable(); }); - it('does allow user with reporting_user role', async () => { + it('does allow user with reporting privileges', async () => { await reportingFunctional.loginReportingUser(); await reportingFunctional.openCanvasWorkpad(CANVAS_TITLE); await reportingFunctional.tryGeneratePdfSuccess(); @@ -93,17 +99,23 @@ export default function ({ getService }: FtrProviderContext) { }); describe('Visualize Editor: Generate Screenshot', () => { - it('does not allow user that does not have reporting_user role', async () => { + it('does not allow user that does not have reporting privileges', async () => { await reportingFunctional.loginDataAnalyst(); await reportingFunctional.openSavedVisualization(VIS_TITLE); - await reportingFunctional.tryGeneratePdfFail(); + await reportingFunctional.tryReportsNotAvailable(); }); - it('does allow user with reporting_user role', async () => { + it('does allow PDF generation user with reporting privileges', async () => { await reportingFunctional.loginReportingUser(); await reportingFunctional.openSavedVisualization(VIS_TITLE); await reportingFunctional.tryGeneratePdfSuccess(); }); + + it('does allow PNG generation user with reporting privileges', async () => { + await reportingFunctional.loginReportingUser(); + await reportingFunctional.openSavedVisualization(VIS_TITLE); + await reportingFunctional.tryGeneratePngSuccess(); + }); }); }); } diff --git a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts index db9715f7e48d5..47d942db947f5 100644 --- a/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts +++ b/x-pack/test/security_api_integration/tests/anonymous/capabilities.ts @@ -56,6 +56,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -63,6 +65,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -76,6 +79,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -87,6 +91,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -94,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -107,6 +114,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -118,6 +126,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -125,6 +135,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -138,6 +149,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -168,6 +180,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -175,6 +189,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -188,6 +203,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -199,6 +215,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -206,6 +224,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -219,6 +238,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -230,6 +250,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -237,6 +259,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -250,6 +273,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -298,6 +322,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": true, "showWriteControls": false, @@ -305,6 +331,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": true, @@ -318,6 +345,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": true, @@ -331,6 +359,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -338,6 +368,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -351,6 +382,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": false, @@ -364,6 +396,8 @@ export default function ({ getService }: FtrProviderContext) { "dashboard": Object { "createNew": false, "createShortUrl": false, + "downloadCsv": false, + "generateScreenshot": false, "saveQuery": false, "show": false, "showWriteControls": false, @@ -371,6 +405,7 @@ export default function ({ getService }: FtrProviderContext) { }, "discover": Object { "createShortUrl": false, + "generateCsv": false, "save": false, "saveQuery": false, "show": false, @@ -384,6 +419,7 @@ export default function ({ getService }: FtrProviderContext) { "visualize": Object { "createShortUrl": false, "delete": false, + "generateScreenshot": false, "save": false, "saveQuery": false, "show": true,