[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:
Tim Sullivan 2021-04-20 20:44:24 -07:00 committed by GitHub
parent e39b8c6d36
commit 5a6eda2b22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
84 changed files with 2316 additions and 705 deletions

View file

@ -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" ]`.
|===

View file

@ -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]]

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View file

@ -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

View file

@ -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

View file

@ -18,6 +18,7 @@
],
"optionalPlugins": [
"home",
"reporting",
"usageCollection"
],
"requiredBundles": [

View file

@ -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) => {

View file

@ -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,

View file

@ -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(

View file

@ -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;

View file

@ -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>;

View file

@ -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;

View 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 };
};

View file

@ -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,

View 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,
};

View 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",
],
},
],
},
],
},
],
}
`);
});

View 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)
: []),
],
};
}

View file

@ -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();

View file

@ -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 {

View file

@ -14,6 +14,7 @@ const createSetup = (): jest.Mocked<PluginSetupContract> => {
getFeaturesUICapabilities: jest.fn(),
registerKibanaFeature: jest.fn(),
registerElasticsearchFeature: jest.fn(),
enableReportingUiCapabilities: jest.fn(),
};
};

View file

@ -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`, () => {

View file

@ -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'],
},
],
},
],
},
};

View file

@ -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;
}
}

View file

@ -18,6 +18,7 @@ export interface ReportingSetup {
};
getDefaultLayoutSelectors: typeof getDefaultLayoutSelectors;
ReportingAPIClient: typeof ReportingAPIClient;
usesUiCapabilities: () => boolean;
}
export type ReportingStart = ReportingSetup;

View file

@ -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);
});
});
});

View file

@ -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 = {

View file

@ -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,
})
);

View file

@ -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,
},

View file

@ -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 };
};

View file

@ -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);

View file

@ -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 []`);
});
});

View file

@ -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;
},
],

View file

@ -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,
},
}
`);

View file

@ -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'] }),
});

View file

@ -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
*/

View file

@ -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);

View file

@ -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 () => {

View file

@ -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 {},
},

View file

@ -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());
});

View file

@ -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,

View file

@ -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 };

View file

@ -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(({

View file

@ -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({

View file

@ -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', () => {

View file

@ -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', () => {

View file

@ -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();
}
}

View file

@ -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']);

View file

@ -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({

View file

@ -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: {

View file

@ -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({

View file

@ -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;

View file

@ -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({

View file

@ -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"`)
);
});
});
});

View file

@ -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 () {});
});

View file

@ -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 });
}
};
};
};

View file

@ -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 || '',
},
});
};
}

View file

@ -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();

View file

@ -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'];
}
/**

View file

@ -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();

View file

@ -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'),

View file

@ -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')

View file

@ -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'],

View file

@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
kibana: [
{
feature: {
canvas: ['all'],
canvas: ['minimal_all'],
},
spaces: ['*'],
},

View file

@ -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);
});

View file

@ -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', () => {

View file

@ -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();

View file

@ -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
);
});

View file

@ -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',

View file

@ -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');
});

View file

@ -262,7 +262,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
kibana: [
{
feature: {
visualize: ['all'],
visualize: ['minimal_all'],
},
spaces: ['*'],
},

View file

@ -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 () {

View file

@ -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 () => {

View file

@ -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: ['*'],
},

View file

@ -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));

View file

@ -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();
});

View file

@ -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,

View file

@ -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
],
},
};
}

View file

@ -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'));
});
}

View file

@ -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');
});
});
};

View file

@ -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();
});
});
});
}

View file

@ -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'));

View file

@ -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');

View file

@ -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();
});
});
});
}

View file

@ -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,