mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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 <kaarina.tungseth@elastic.co> * Apply suggestions from code review Co-authored-by: Kaarina Tungseth <kaarina.tungseth@elastic.co> * Apply suggestions from code review Co-authored-by: Kaarina Tungseth <kaarina.tungseth@elastic.co> * typo fix * "PDF / PNG Reports" => "Reporting" * Update x-pack/plugins/reporting/server/config/index.ts Co-authored-by: Larry Gregory <lgregorydev@gmail.com> * Update x-pack/test/functional/apps/security/secure_roles_perm.js Co-authored-by: Larry Gregory <lgregorydev@gmail.com> * 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 <kaarina.tungseth@elastic.co> Co-authored-by: Larry Gregory <lgregorydev@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com>
This commit is contained in:
parent
e39b8c6d36
commit
5a6eda2b22
84 changed files with 2316 additions and 705 deletions
|
@ -275,9 +275,20 @@ For information about {kib} memory limits, see <<production, using {kib} in a pr
|
|||
every {kib} instance that has a unique <<kibana-index, `kibana.index`>>
|
||||
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" ]`.
|
||||
|
||||
|===
|
||||
|
||||
|
|
|
@ -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 <<secure-reporting, Reporting and security>>
|
||||
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
|
||||
<<kibana-privileges, {kib} privileges>>. It is recommended that *Reporting* is configured to
|
||||
use {kib} privileges by setting <<xpack-reporting-roles-enabled,`xpack.reporting.roles.enabled`>> 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 <<secure-reporting, Reporting and
|
||||
security>>.
|
||||
|
||||
[float]
|
||||
[[manually-generate-reports]]
|
||||
|
|
BIN
docs/user/security/images/reporting-custom-role.png
Normal file
BIN
docs/user/security/images/reporting-custom-role.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 148 KiB |
|
@ -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 <<kibana-privileges, {kib} privileges>> 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 <<kibana-privileges, {kib} privileges>>.
|
||||
|
||||
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 <<reporting-roles-user-api, user API>>.
|
||||
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 <<reporting-roles-user-api, user API>>. 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
],
|
||||
"optionalPlugins": [
|
||||
"home",
|
||||
"reporting",
|
||||
"usageCollection"
|
||||
],
|
||||
"requiredBundles": [
|
||||
|
|
|
@ -12,6 +12,7 @@ import { ShareMenu } from '../share_menu.component';
|
|||
|
||||
storiesOf('components/WorkpadHeader/ShareMenu', module).add('default', () => (
|
||||
<ShareMenu
|
||||
includeReporting={true}
|
||||
onCopy={action('onCopy')}
|
||||
onExport={action('onExport')}
|
||||
getExportUrl={(type: string) => {
|
||||
|
|
|
@ -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<Props> = ({ onCopy, onExport, getExportUrl }) => {
|
||||
export const ShareMenu: FunctionComponent<Props> = ({
|
||||
includeReporting,
|
||||
onCopy,
|
||||
onExport,
|
||||
getExportUrl,
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState(false);
|
||||
|
||||
const onClose = () => {
|
||||
|
@ -73,16 +80,18 @@ export const ShareMenu: FunctionComponent<Props> = ({ 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: <EuiIcon type="globe" size="m" />,
|
||||
|
@ -91,7 +100,7 @@ export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExpor
|
|||
closePopover();
|
||||
},
|
||||
},
|
||||
],
|
||||
].filter(Boolean),
|
||||
});
|
||||
|
||||
const shareControl = (togglePopover: React.MouseEventHandler<any>) => (
|
||||
|
@ -123,6 +132,7 @@ export const ShareMenu: FunctionComponent<Props> = ({ onCopy, onExport, getExpor
|
|||
};
|
||||
|
||||
ShareMenu.propTypes = {
|
||||
includeReporting: PropTypes.bool.isRequired,
|
||||
onCopy: PropTypes.func.isRequired,
|
||||
onExport: PropTypes.func.isRequired,
|
||||
getExportUrl: PropTypes.func.isRequired,
|
||||
|
|
|
@ -46,6 +46,7 @@ export const ShareMenu = compose<ComponentProps, {}>(
|
|||
withServices,
|
||||
withProps(
|
||||
({ workpad, pageCount, services }: Props & WithServicesProps): ComponentProps => ({
|
||||
includeReporting: services.reporting.includeReporting(),
|
||||
getExportUrl: (type, layout) => {
|
||||
if (type === 'pdf') {
|
||||
const pdfUrl = getPdfUrl(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 <context.Provider value={value}>{children}</context.Provider>;
|
||||
|
|
|
@ -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<typeof services.notify>;
|
||||
platform: ServiceFromProvider<typeof services.platform>;
|
||||
navLink: ServiceFromProvider<typeof services.navLink>;
|
||||
reporting: ServiceFromProvider<typeof services.reporting>;
|
||||
labs: ServiceFromProvider<typeof services.labs>;
|
||||
}
|
||||
|
||||
|
@ -117,4 +120,5 @@ export const {
|
|||
platform: platformService,
|
||||
navLink: navLinkService,
|
||||
expressions: expressionsService,
|
||||
reporting: reportingService,
|
||||
} = services;
|
||||
|
|
35
x-pack/plugins/canvas/public/services/reporting.ts
Normal file
35
x-pack/plugins/canvas/public/services/reporting.ts
Normal file
|
@ -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<ReportingService> = (
|
||||
_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 };
|
||||
};
|
|
@ -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,
|
||||
|
|
12
x-pack/plugins/canvas/public/services/stubs/reporting.ts
Normal file
12
x-pack/plugins/canvas/public/services/stubs/reporting.ts
Normal file
|
@ -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,
|
||||
};
|
193
x-pack/plugins/canvas/server/feature.test.ts
Normal file
193
x-pack/plugins/canvas/server/feature.test.ts
Normal file
|
@ -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",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
81
x-pack/plugins/canvas/server/feature.ts
Normal file
81
x-pack/plugins/canvas/server/feature.ts
Normal file
|
@ -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)
|
||||
: []),
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -14,6 +14,7 @@ const createSetup = (): jest.Mocked<PluginSetupContract> => {
|
|||
getFeaturesUICapabilities: jest.fn(),
|
||||
registerKibanaFeature: jest.fn(),
|
||||
registerElasticsearchFeature: jest.fn(),
|
||||
enableReportingUiCapabilities: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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`, () => {
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export interface ReportingSetup {
|
|||
};
|
||||
getDefaultLayoutSelectors: typeof getDefaultLayoutSelectors;
|
||||
ReportingAPIClient: typeof ReportingAPIClient;
|
||||
usesUiCapabilities: () => boolean;
|
||||
}
|
||||
|
||||
export type ReportingStart = ReportingSetup;
|
||||
|
|
|
@ -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<typeof mockStartServicesPayload>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ActionContext> {
|
||||
interface Params {
|
||||
core: CoreSetup;
|
||||
startServices$: Rx.Observable<[CoreStart, object, unknown]>;
|
||||
license$: LicensingPluginSetup['license$'];
|
||||
usesUiCapabilities: boolean;
|
||||
}
|
||||
|
||||
export class ReportingCsvPanelAction implements ActionDefinition<ActionContext> {
|
||||
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<ActionContext>
|
|||
}
|
||||
|
||||
public isCompatible = async (context: ActionContext) => {
|
||||
if (!this.canDownloadCSV) {
|
||||
if (!this.licenseHasDownloadCsv || !this.capabilityHasDownloadCsv) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -82,7 +100,7 @@ export class GetCsvReportPanelAction implements ActionDefinition<ActionContext>
|
|||
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<ActionContext>
|
|||
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 = {
|
||||
|
|
|
@ -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<ClientConfigType>();
|
||||
|
||||
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,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
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 (!licenseHasScreenshotReporting) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!['dashboard', 'visualization'].includes(objectType)) {
|
||||
return [];
|
||||
}
|
||||
// Dashboard only mode does not currently support reporting
|
||||
// https://github.com/elastic/kibana/issues/18286
|
||||
// @TODO For NP
|
||||
if (objectType === 'dashboard' && false) {
|
||||
|
||||
if (objectType === 'dashboard' && !capabilityHasDashboardScreenshotReporting) {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
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 pngPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pngReportsButtonLabel', {
|
||||
defaultMessage: 'PNG Reports',
|
||||
});
|
||||
|
||||
const pdfPanelTitle = i18n.translate(
|
||||
'xpack.reporting.shareContextMenu.pdfReportsButtonLabel',
|
||||
{
|
||||
defaultMessage: 'PDF Reports',
|
||||
}
|
||||
);
|
||||
const panelPng = {
|
||||
shareMenuItem: {
|
||||
name: pngPanelTitle,
|
||||
icon: 'document',
|
||||
toolTipContent: licenseToolTipContent,
|
||||
disabled: licenseDisabled,
|
||||
['data-test-subj']: 'pngReportMenuItem',
|
||||
sortOrder: 10,
|
||||
},
|
||||
panel: {
|
||||
id: 'reportingPngPanel',
|
||||
title: pngPanelTitle,
|
||||
content: (
|
||||
<ScreenCapturePanelContent
|
||||
apiClient={apiClient}
|
||||
toasts={toasts}
|
||||
reportType="png"
|
||||
objectId={objectId}
|
||||
getJobParams={getPngJobParams({
|
||||
shareableUrl,
|
||||
apiClient,
|
||||
objectType,
|
||||
browserTimezone,
|
||||
sharingData,
|
||||
})}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: pngPanelTitle,
|
||||
icon: 'document',
|
||||
toolTipContent,
|
||||
disabled,
|
||||
['data-test-subj']: 'pngReportMenuItem',
|
||||
sortOrder: 10,
|
||||
},
|
||||
panel: {
|
||||
id: 'reportingPngPanel',
|
||||
title: pngPanelTitle,
|
||||
content: (
|
||||
<ScreenCapturePanelContent
|
||||
apiClient={apiClient}
|
||||
toasts={toasts}
|
||||
reportType="png"
|
||||
objectId={objectId}
|
||||
getJobParams={getPngJobParams}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
const pdfPanelTitle = i18n.translate('xpack.reporting.shareContextMenu.pdfReportsButtonLabel', {
|
||||
defaultMessage: 'PDF Reports',
|
||||
});
|
||||
|
||||
shareActions.push({
|
||||
shareMenuItem: {
|
||||
name: pdfPanelTitle,
|
||||
icon: 'document',
|
||||
toolTipContent,
|
||||
disabled,
|
||||
['data-test-subj']: 'pdfReportMenuItem',
|
||||
sortOrder: 10,
|
||||
},
|
||||
panel: {
|
||||
id: 'reportingPdfPanel',
|
||||
title: pdfPanelTitle,
|
||||
content: (
|
||||
<ScreenCapturePanelContent
|
||||
apiClient={apiClient}
|
||||
toasts={toasts}
|
||||
reportType="printablePdf"
|
||||
objectId={objectId}
|
||||
getJobParams={getPdfJobParams}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
const panelPdf = {
|
||||
shareMenuItem: {
|
||||
name: pdfPanelTitle,
|
||||
icon: 'document',
|
||||
toolTipContent: licenseToolTipContent,
|
||||
disabled: licenseDisabled,
|
||||
['data-test-subj']: 'pdfReportMenuItem',
|
||||
sortOrder: 10,
|
||||
},
|
||||
panel: {
|
||||
id: 'reportingPdfPanel',
|
||||
title: pdfPanelTitle,
|
||||
content: (
|
||||
<ScreenCapturePanelContent
|
||||
apiClient={apiClient}
|
||||
toasts={toasts}
|
||||
reportType="printablePdf"
|
||||
objectId={objectId}
|
||||
getJobParams={getPdfJobParams({
|
||||
shareableUrl,
|
||||
apiClient,
|
||||
objectType,
|
||||
browserTimezone,
|
||||
sharingData,
|
||||
})}
|
||||
isDirty={isDirty}
|
||||
onClose={onClose}
|
||||
/>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
shareActions.push(panelPng);
|
||||
shareActions.push(panelPdf);
|
||||
return shareActions;
|
||||
};
|
||||
|
||||
return {
|
||||
id: 'screenCaptureReports',
|
||||
getShareMenuItems,
|
||||
};
|
||||
return { id: 'screenCaptureReports', getShareMenuItems };
|
||||
};
|
||||
|
|
|
@ -103,6 +103,9 @@ describe('Reporting server createConfig$', () => {
|
|||
"pollInterval": 3000,
|
||||
"timeout": 120000,
|
||||
},
|
||||
"roles": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect((mockLogger.warn as any).mock.calls.length).toBe(0);
|
||||
|
|
|
@ -32,7 +32,7 @@ const applyReportingDeprecations = (settings: Record<string, any> = {}) => {
|
|||
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 []`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ReportingConfigType> = {
|
||||
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<ReportingConfigType> = {
|
|||
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;
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
|
|
@ -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'] }),
|
||||
});
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
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';
|
||||
|
@ -30,8 +31,7 @@ import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib';
|
|||
import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots';
|
||||
import { ReportingStore } from './lib/store';
|
||||
import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks';
|
||||
import { ReportingPluginRouter } from './types';
|
||||
import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server';
|
||||
import { ReportingPluginRouter, ReportingStart } from './types';
|
||||
|
||||
export interface ReportingInternalSetup {
|
||||
basePath: Pick<BasePath, 'set'>;
|
||||
|
@ -41,6 +41,7 @@ export interface ReportingInternalSetup {
|
|||
security?: SecurityPluginSetup;
|
||||
spaces?: SpacesPluginSetup;
|
||||
taskManager: TaskManagerSetupContract;
|
||||
logger: LevelLogger;
|
||||
}
|
||||
|
||||
export interface ReportingInternalStart {
|
||||
|
@ -51,6 +52,7 @@ export interface ReportingInternalStart {
|
|||
esClient: IClusterClient;
|
||||
data: DataPluginStart;
|
||||
taskManager: TaskManagerStartContract;
|
||||
logger: LevelLogger;
|
||||
}
|
||||
|
||||
export class ReportingCore {
|
||||
|
@ -58,16 +60,27 @@ export class ReportingCore {
|
|||
private pluginStartDeps?: ReportingInternalStart;
|
||||
private readonly pluginSetup$ = new Rx.ReplaySubject<boolean>(); // observe async background setupDeps and config each are done
|
||||
private readonly pluginStart$ = new Rx.ReplaySubject<ReportingInternalStart>(); // observe async background startDeps
|
||||
private deprecatedAllowedRoles: string[] | false = false; // DEPRECATED. If `false`, the deprecated features have been disableed
|
||||
private exportTypesRegistry = getExportTypesRegistry();
|
||||
private executeTask: ExecuteReportTask;
|
||||
private monitorTask: MonitorReportsTask;
|
||||
private config?: ReportingConfig;
|
||||
private config?: ReportingConfig; // final config, includes dynamic values based on OS type
|
||||
private executing: Set<string>;
|
||||
|
||||
public getStartContract: () => ReportingStart;
|
||||
|
||||
constructor(private logger: LevelLogger, context: PluginInitializerContext<ReportingConfigType>) {
|
||||
const config = context.config.get<ReportingConfigType>();
|
||||
this.executeTask = new ExecuteReportTask(this, config, this.logger);
|
||||
this.monitorTask = new MonitorReportsTask(this, config, this.logger);
|
||||
const syncConfig = context.config.get<ReportingConfigType>();
|
||||
this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false;
|
||||
this.executeTask = new ExecuteReportTask(this, syncConfig, this.logger);
|
||||
this.monitorTask = new MonitorReportsTask(this, syncConfig, this.logger);
|
||||
|
||||
this.getStartContract = (): ReportingStart => {
|
||||
return {
|
||||
usesUiCapabilities: () => syncConfig.roles.enabled === false,
|
||||
};
|
||||
};
|
||||
|
||||
this.executing = new Set();
|
||||
}
|
||||
|
||||
|
@ -132,23 +145,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();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -161,6 +189,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
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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<string, string>) => {
|
|||
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 {},
|
||||
},
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ReportingConfigType>) =>
|
||||
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 };
|
||||
|
|
|
@ -10,7 +10,6 @@ import { ReportingCore } from '../';
|
|||
import { TaskManagerStartContract } from '../../../task_manager/server';
|
||||
import { ReportingInternalStart } from '../core';
|
||||
import {
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
|
@ -23,8 +22,6 @@ import { TaskRunResult } from './tasks';
|
|||
|
||||
describe('Enqueue Job', () => {
|
||||
const logger = createMockLevelLogger();
|
||||
const mockSchema = createMockConfigSchema();
|
||||
const mockConfig = createMockConfig(mockSchema);
|
||||
let mockReporting: ReportingCore;
|
||||
let mockExportTypesRegistry: ExportTypesRegistry;
|
||||
|
||||
|
@ -42,7 +39,7 @@ describe('Enqueue Job', () => {
|
|||
runTaskFnFactory: () => async () =>
|
||||
(({ runParamsTest: { test2: 'yes' } } as unknown) as TaskRunResult),
|
||||
});
|
||||
mockReporting = await createMockReportingCore(mockConfig);
|
||||
mockReporting = await createMockReportingCore(createMockConfigSchema());
|
||||
mockReporting.getExportTypesRegistry = () => mockExportTypesRegistry;
|
||||
mockReporting.getStore = () =>
|
||||
Promise.resolve(({
|
||||
|
|
|
@ -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<ElasticsearchClient>;
|
||||
|
||||
|
@ -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({
|
||||
|
|
|
@ -10,7 +10,6 @@ import { RunContext } from '../../../../task_manager/server';
|
|||
import { taskManagerMock } from '../../../../task_manager/server/mocks';
|
||||
import { ReportingConfigType } from '../../config';
|
||||
import {
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
|
@ -24,8 +23,7 @@ describe('Execute Report Task', () => {
|
|||
let configType: ReportingConfigType;
|
||||
beforeAll(async () => {
|
||||
configType = createMockConfigSchema();
|
||||
const mockConfig = createMockConfig(configType);
|
||||
mockReporting = await createMockReportingCore(mockConfig);
|
||||
mockReporting = await createMockReportingCore(configType);
|
||||
});
|
||||
|
||||
it('Instance setup', () => {
|
||||
|
|
|
@ -10,7 +10,6 @@ import { RunContext } from '../../../../task_manager/server';
|
|||
import { taskManagerMock } from '../../../../task_manager/server/mocks';
|
||||
import { ReportingConfigType } from '../../config';
|
||||
import {
|
||||
createMockConfig,
|
||||
createMockConfigSchema,
|
||||
createMockLevelLogger,
|
||||
createMockReportingCore,
|
||||
|
@ -24,8 +23,7 @@ describe('Execute Report Task', () => {
|
|||
let configType: ReportingConfigType;
|
||||
beforeAll(async () => {
|
||||
configType = createMockConfigSchema();
|
||||
const mockConfig = createMockConfig(configType);
|
||||
mockReporting = await createMockReportingCore(mockConfig);
|
||||
mockReporting = await createMockReportingCore(configType);
|
||||
});
|
||||
|
||||
it('Instance setup', () => {
|
||||
|
|
|
@ -24,23 +24,23 @@ import { registerReportingUsageCollector } from './usage';
|
|||
|
||||
export class ReportingPlugin
|
||||
implements Plugin<ReportingSetup, ReportingStart, ReportingSetupDeps, ReportingStartDeps> {
|
||||
private readonly initializerContext: PluginInitializerContext<ReportingConfigType>;
|
||||
private logger: LevelLogger;
|
||||
private reportingCore: ReportingCore;
|
||||
private reportingCore?: ReportingCore;
|
||||
|
||||
constructor(context: PluginInitializerContext<ReportingConfigType>) {
|
||||
this.logger = new LevelLogger(context.logger.get());
|
||||
this.reportingCore = new ReportingCore(this.logger, context);
|
||||
this.initializerContext = context;
|
||||
constructor(private initContext: PluginInitializerContext<ReportingConfigType>) {
|
||||
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, taskManager } = plugins;
|
||||
const { initializerContext: initContext, reportingCore } = this;
|
||||
|
||||
const router = http.createRouter<ReportingRequestHandlerContext>();
|
||||
const basePath = http.basePath;
|
||||
|
@ -62,6 +61,7 @@ export class ReportingPlugin
|
|||
security,
|
||||
spaces,
|
||||
taskManager,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
registerReportingUsageCollector(reportingCore, plugins);
|
||||
|
@ -69,7 +69,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();
|
||||
|
@ -79,22 +79,22 @@ 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);
|
||||
|
||||
await reportingCore.pluginStart({
|
||||
browserDriverFactory,
|
||||
|
@ -104,6 +104,7 @@ export class ReportingPlugin
|
|||
esClient: core.elasticsearch.client,
|
||||
data: plugins.data,
|
||||
taskManager: plugins.taskManager,
|
||||
logger: this.logger,
|
||||
});
|
||||
|
||||
this.logger.debug('Start complete');
|
||||
|
@ -112,6 +113,6 @@ export class ReportingPlugin
|
|||
this.logger.error(e);
|
||||
});
|
||||
|
||||
return {};
|
||||
return reportingCore.getStartContract();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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<ReportingRequestHandlerContext, 'reporting'>(
|
||||
reportingSymbol,
|
||||
'reporting',
|
||||
() => ({})
|
||||
() => ({ usesUiCapabilities: () => false })
|
||||
);
|
||||
|
||||
const mockSetupDeps = createMockPluginSetup({
|
||||
|
|
|
@ -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<ReturnType<typeof setupServer>>;
|
||||
|
||||
|
@ -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<ElasticsearchClient>;
|
||||
|
||||
const mockLogger = createMockLevelLogger();
|
||||
|
@ -37,26 +40,14 @@ describe('POST /diagnose/config', () => {
|
|||
httpSetup.registerRouteHandlerContext<ReportingRequestHandlerContext, 'reporting'>(
|
||||
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: {
|
||||
|
|
|
@ -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<ReportingRequestHandlerContext, 'reporting'>(
|
||||
reportingSymbol,
|
||||
'reporting',
|
||||
() => ({})
|
||||
() => ({ usesUiCapabilities: () => false })
|
||||
);
|
||||
|
||||
const mockSetupDeps = createMockPluginSetup({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ElasticsearchClient>;
|
||||
|
||||
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<ReportingRequestHandlerContext, 'reporting'>(
|
||||
reportingSymbol,
|
||||
'reporting',
|
||||
() => ({})
|
||||
() => ({ usesUiCapabilities: jest.fn() })
|
||||
);
|
||||
|
||||
const mockSetupDeps = createMockPluginSetup({
|
||||
|
|
|
@ -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<ElasticsearchClient>;
|
||||
|
||||
const config = createMockConfig(createMockConfigSchema());
|
||||
const getHits = (...sources: any) => {
|
||||
return {
|
||||
hits: {
|
||||
|
@ -47,9 +46,9 @@ describe('GET /api/reporting/jobs/download', () => {
|
|||
httpSetup.registerRouteHandlerContext<ReportingRequestHandlerContext, 'reporting'>(
|
||||
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"`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 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 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);
|
||||
// @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);
|
||||
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'] }),
|
||||
},
|
||||
},
|
||||
} 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);
|
||||
});
|
||||
|
||||
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 () {});
|
||||
});
|
||||
|
|
|
@ -26,35 +26,40 @@ export type RequestHandlerUser<P, Q, B> = 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 <P, Q, B>(
|
||||
handler: RequestHandlerUser<P, Q, B>
|
||||
): RequestHandler<P, Q, B, ReportingRequestHandlerContext, RouteMethod> => {
|
||||
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 });
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -32,37 +32,42 @@ export function downloadJobResponseHandlerFactory(reporting: ReportingCore) {
|
|||
params: JobResponseHandlerParams,
|
||||
opts: JobResponseHandlerOpts = {}
|
||||
) {
|
||||
const { docId } = params;
|
||||
try {
|
||||
const { docId } = params;
|
||||
|
||||
const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent });
|
||||
if (!doc) {
|
||||
return res.notFound();
|
||||
}
|
||||
const doc = await jobsQuery.get(user, docId, { includeContent: !opts.excludeContent });
|
||||
if (!doc) {
|
||||
return res.notFound();
|
||||
}
|
||||
|
||||
const { jobtype: jobType } = doc._source;
|
||||
const { jobtype: jobType } = doc._source;
|
||||
|
||||
if (!validJobTypes.includes(jobType)) {
|
||||
return res.unauthorized({
|
||||
body: `Sorry, you are not authorized to download ${jobType} reports`,
|
||||
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);
|
||||
}
|
||||
|
||||
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 || '',
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup =
|
|||
security: setupMock.security,
|
||||
licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any,
|
||||
taskManager: { registerTaskDefinitions: jest.fn() } as any,
|
||||
logger: createMockLevelLogger(),
|
||||
...setupMock,
|
||||
};
|
||||
};
|
||||
|
@ -69,6 +70,7 @@ export const createMockPluginStart = (
|
|||
schedule: jest.fn().mockImplementation(() => ({ id: 'taskId' })),
|
||||
ensureScheduled: jest.fn(),
|
||||
} as any,
|
||||
logger: createMockLevelLogger(),
|
||||
...startMock,
|
||||
};
|
||||
};
|
||||
|
@ -79,6 +81,7 @@ interface ReportingConfigTestType {
|
|||
queue: Partial<ReportingConfigType['queue']>;
|
||||
kibanaServer: Partial<ReportingConfigType['kibanaServer']>;
|
||||
csv: Partial<ReportingConfigType['csv']>;
|
||||
roles?: Partial<ReportingConfigType['roles']>;
|
||||
capture: any;
|
||||
server?: any;
|
||||
}
|
||||
|
@ -114,6 +117,10 @@ export const createMockConfigSchema = (
|
|||
csv: {
|
||||
...overrides.csv,
|
||||
},
|
||||
roles: {
|
||||
enabled: false,
|
||||
...overrides.roles,
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
|
@ -130,12 +137,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;
|
||||
|
@ -148,8 +155,10 @@ export const createMockReportingCore = async (
|
|||
}
|
||||
|
||||
const context = coreMock.createPluginInitializerContext(createMockConfigSchema());
|
||||
context.config = { get: () => config } as any;
|
||||
|
||||
const core = new ReportingCore(logger, context);
|
||||
core.setConfig(config);
|
||||
core.setConfig(createMockConfig(config));
|
||||
|
||||
core.pluginSetup(setupDepsMock);
|
||||
await core.pluginSetsUp();
|
||||
|
|
|
@ -39,8 +39,11 @@ export interface ReportingStartDeps {
|
|||
taskManager: TaskManagerStartContract;
|
||||
}
|
||||
|
||||
export type ReportingStart = object;
|
||||
export type ReportingSetup = object;
|
||||
export interface ReportingSetup {
|
||||
usesUiCapabilities: () => boolean;
|
||||
}
|
||||
|
||||
export type ReportingStart = ReportingSetup;
|
||||
|
||||
/*
|
||||
* Internal Types
|
||||
|
@ -100,8 +103,9 @@ export interface ExportTypeDefinition<
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface ReportingRequestHandlerContext extends RequestHandlerContext {
|
||||
export interface ReportingRequestHandlerContext {
|
||||
reporting: ReportingStart | null;
|
||||
core: RequestHandlerContext['core'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -14,6 +14,7 @@ const alwaysImportedTests = [
|
|||
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'),
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
canvas: ['all'],
|
||||
canvas: ['minimal_all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
||||
|
|
|
@ -262,7 +262,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
visualize: ['all'],
|
||||
visualize: ['minimal_all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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: ['*'],
|
||||
},
|
||||
|
|
|
@ -34,7 +34,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 +50,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 +66,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 +82,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));
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -58,6 +58,35 @@ export function createScenarios({ getService }: Pick<FtrProviderContext, 'getSer
|
|||
});
|
||||
};
|
||||
|
||||
const createTestReportingUserRole = async () => {
|
||||
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<FtrProviderContext, 'getSer
|
|||
const createTestReportingUser = async () => {
|
||||
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<FtrProviderContext, 'getSer
|
|||
REPORTING_USER_PASSWORD,
|
||||
createDataAnalystRole,
|
||||
createDataAnalyst,
|
||||
createTestReportingUserRole,
|
||||
createTestReportingUser,
|
||||
downloadCsv,
|
||||
generatePdf,
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
|
||||
import { resolve } from 'path';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const reportingConfig = await readConfigFile(require.resolve('./reporting_and_security.config'));
|
||||
|
||||
return {
|
||||
...reportingConfig.getAll(),
|
||||
junit: { reportName: 'X-Pack Reporting Functional Tests With Deprecated Roles config' },
|
||||
testFiles: [resolve(__dirname, './reporting_and_deprecated_security')],
|
||||
kbnTestServer: {
|
||||
...reportingConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...reportingConfig.get('kbnTestServer.serverArgs'),
|
||||
`--xpack.reporting.roles.enabled=true`, // DEPRECATED: support for `true` will be removed in 8.0
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 function (context: FtrProviderContext) {
|
||||
const security = context.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'], // 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'));
|
||||
});
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue