Skip to content

Commit 00f077b

Browse files
darnautovtsullivankobelb
authored
[9.0] Add reporting_user feature for reserved set of privileges (#231533) (#232396)
# Backport This will backport the following commits from `main` to `9.0`: - [Add `reporting_user` feature for reserved set of privileges (#231533)](#231533) <!--- Backport version: 10.0.1 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Tim Sullivan","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-08-20T11:57:52Z","message":"Add `reporting_user` feature for reserved set of privileges (#231533)\n\n## Summary\n\nWe want to switch the reserved `reporting_user` role to use a \"reserved\nprivilege definition\" and uses just that privilege. This PR satisfies\nthe Kibana requirements. There is a corresponding Elasticsearch PR:\nhttps://github.com/elastic/elasticsearch/pull/132766\n\n## Testing\n**NOTE: PNG/PDF reporting requires a Trial, or Gold+ license**\n\n1. Create `test_reporting_user` role\n\n ```\n POST /_security/role/test_reporting_user\n {\n \"cluster\": [],\n \"indices\": [],\n \"application\": [{\n \"application\": \"kibana-*\",\n \"privileges\": [\"reserved_reporting_user\"],\n \"resources\": [\"*\"]\n }]\n }\n ```\n\n2. Create `test_analyst_user` role\n\n ```\n POST /_security/role/test_analyst_user\n {\n \"cluster\": [],\n \"indices\": [\n {\n \"names\": [\"kibana_sample_*\"],\n \"privileges\": [\"all\"],\n \"field_security\": {\n \"grant\": [\"*\"],\n \"except\": []\n },\n \"allow_restricted_indices\": false\n }\n ],\n \"applications\": [\n {\n \"application\": \"kibana-.kibana\",\n \"privileges\": [\n \"feature_discover_v2.read\",\n \"feature_dashboard_v2.read\",\n \"feature_canvas.read\",\n \"feature_visualize_v2.read\"\n ],\n \"resources\": [\"space:default\"]\n }\n ],\n \"run_as\": [],\n \"metadata\": {},\n \"transient_metadata\": {\n \"enabled\": true\n }\n }\n ```\n\n3. Create a test user with just those two roles. Install sample data.\nLog in using the new test user.\n4. Test cases\n\n | App | Reporting feature\n |-|-\n | Dashboard | PDF, PNG, CSV (from saved search panel action)\n | Discover | CSV\n | Canvas | PDF\n | Lens | PDF, PNG\n| Stack Management | List reports, download reports, view report info,\ndelete reports\n\n6. As admin, create an additional Space which the test user should not\nhave access to. Ensure the test user does not have access to those\nspaces.\n7. Remove the `test_reporting_user` role from the user and ensure they\ndo not see any Reporting controls in the UI, and can not access Stack\nManagement > Reporting.\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- ~~[ ] Any text added follows [EUI's writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\nsentence case text and includes [i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)~~\n- ~~[ ]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas added for features that require explanation or tutorials~~\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- ~~[ ] If a plugin configuration key changed, check if it needs to be\nallowlisted in the cloud and added to the [docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~~\n- ~~[ ] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.~~\n- ~~[ ] [Flaky Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\nused on any tests changed~~\n- [ ] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n---------\n\nCo-authored-by: Larry Gregory <[email protected]>","sha":"f9be58be65e59b85dc6c4d8fa74970a4f8c1971e","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:version","v9.2.0","v9.1.3","v9.0.6"],"title":"Add `reporting_user` feature for reserved set of privileges","number":231533,"url":"https://github.com/elastic/kibana/pull/231533","mergeCommit":{"message":"Add `reporting_user` feature for reserved set of privileges (#231533)\n\n## Summary\n\nWe want to switch the reserved `reporting_user` role to use a \"reserved\nprivilege definition\" and uses just that privilege. This PR satisfies\nthe Kibana requirements. There is a corresponding Elasticsearch PR:\nhttps://github.com/elastic/elasticsearch/pull/132766\n\n## Testing\n**NOTE: PNG/PDF reporting requires a Trial, or Gold+ license**\n\n1. Create `test_reporting_user` role\n\n ```\n POST /_security/role/test_reporting_user\n {\n \"cluster\": [],\n \"indices\": [],\n \"application\": [{\n \"application\": \"kibana-*\",\n \"privileges\": [\"reserved_reporting_user\"],\n \"resources\": [\"*\"]\n }]\n }\n ```\n\n2. Create `test_analyst_user` role\n\n ```\n POST /_security/role/test_analyst_user\n {\n \"cluster\": [],\n \"indices\": [\n {\n \"names\": [\"kibana_sample_*\"],\n \"privileges\": [\"all\"],\n \"field_security\": {\n \"grant\": [\"*\"],\n \"except\": []\n },\n \"allow_restricted_indices\": false\n }\n ],\n \"applications\": [\n {\n \"application\": \"kibana-.kibana\",\n \"privileges\": [\n \"feature_discover_v2.read\",\n \"feature_dashboard_v2.read\",\n \"feature_canvas.read\",\n \"feature_visualize_v2.read\"\n ],\n \"resources\": [\"space:default\"]\n }\n ],\n \"run_as\": [],\n \"metadata\": {},\n \"transient_metadata\": {\n \"enabled\": true\n }\n }\n ```\n\n3. Create a test user with just those two roles. Install sample data.\nLog in using the new test user.\n4. Test cases\n\n | App | Reporting feature\n |-|-\n | Dashboard | PDF, PNG, CSV (from saved search panel action)\n | Discover | CSV\n | Canvas | PDF\n | Lens | PDF, PNG\n| Stack Management | List reports, download reports, view report info,\ndelete reports\n\n6. As admin, create an additional Space which the test user should not\nhave access to. Ensure the test user does not have access to those\nspaces.\n7. Remove the `test_reporting_user` role from the user and ensure they\ndo not see any Reporting controls in the UI, and can not access Stack\nManagement > Reporting.\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- ~~[ ] Any text added follows [EUI's writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\nsentence case text and includes [i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)~~\n- ~~[ ]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas added for features that require explanation or tutorials~~\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- ~~[ ] If a plugin configuration key changed, check if it needs to be\nallowlisted in the cloud and added to the [docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~~\n- ~~[ ] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.~~\n- ~~[ ] [Flaky Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\nused on any tests changed~~\n- [ ] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n---------\n\nCo-authored-by: Larry Gregory <[email protected]>","sha":"f9be58be65e59b85dc6c4d8fa74970a4f8c1971e"}},"sourceBranch":"main","suggestedTargetBranches":["9.1","9.0"],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/231533","number":231533,"mergeCommit":{"message":"Add `reporting_user` feature for reserved set of privileges (#231533)\n\n## Summary\n\nWe want to switch the reserved `reporting_user` role to use a \"reserved\nprivilege definition\" and uses just that privilege. This PR satisfies\nthe Kibana requirements. There is a corresponding Elasticsearch PR:\nhttps://github.com/elastic/elasticsearch/pull/132766\n\n## Testing\n**NOTE: PNG/PDF reporting requires a Trial, or Gold+ license**\n\n1. Create `test_reporting_user` role\n\n ```\n POST /_security/role/test_reporting_user\n {\n \"cluster\": [],\n \"indices\": [],\n \"application\": [{\n \"application\": \"kibana-*\",\n \"privileges\": [\"reserved_reporting_user\"],\n \"resources\": [\"*\"]\n }]\n }\n ```\n\n2. Create `test_analyst_user` role\n\n ```\n POST /_security/role/test_analyst_user\n {\n \"cluster\": [],\n \"indices\": [\n {\n \"names\": [\"kibana_sample_*\"],\n \"privileges\": [\"all\"],\n \"field_security\": {\n \"grant\": [\"*\"],\n \"except\": []\n },\n \"allow_restricted_indices\": false\n }\n ],\n \"applications\": [\n {\n \"application\": \"kibana-.kibana\",\n \"privileges\": [\n \"feature_discover_v2.read\",\n \"feature_dashboard_v2.read\",\n \"feature_canvas.read\",\n \"feature_visualize_v2.read\"\n ],\n \"resources\": [\"space:default\"]\n }\n ],\n \"run_as\": [],\n \"metadata\": {},\n \"transient_metadata\": {\n \"enabled\": true\n }\n }\n ```\n\n3. Create a test user with just those two roles. Install sample data.\nLog in using the new test user.\n4. Test cases\n\n | App | Reporting feature\n |-|-\n | Dashboard | PDF, PNG, CSV (from saved search panel action)\n | Discover | CSV\n | Canvas | PDF\n | Lens | PDF, PNG\n| Stack Management | List reports, download reports, view report info,\ndelete reports\n\n6. As admin, create an additional Space which the test user should not\nhave access to. Ensure the test user does not have access to those\nspaces.\n7. Remove the `test_reporting_user` role from the user and ensure they\ndo not see any Reporting controls in the UI, and can not access Stack\nManagement > Reporting.\n\n## Checklist\n\nCheck the PR satisfies following conditions. \n\nReviewers should verify this PR satisfies this list as well.\n\n- ~~[ ] Any text added follows [EUI's writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing), uses\nsentence case text and includes [i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)~~\n- ~~[ ]\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\nwas added for features that require explanation or tutorials~~\n- [x] [Unit or functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere updated or added to match the most common scenarios\n- ~~[ ] If a plugin configuration key changed, check if it needs to be\nallowlisted in the cloud and added to the [docker\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~~\n- ~~[ ] This was checked for breaking HTTP API changes, and any breaking\nchanges have been approved by the breaking-change committee. The\n`release_note:breaking` label should be applied in these situations.~~\n- ~~[ ] [Flaky Test\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was\nused on any tests changed~~\n- [ ] The PR description includes the appropriate Release Notes section,\nand the correct `release_note:*` label is applied per the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n- [x] Review the [backport\nguidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing)\nand apply applicable `backport:*` labels.\n\n---------\n\nCo-authored-by: Larry Gregory <[email protected]>","sha":"f9be58be65e59b85dc6c4d8fa74970a4f8c1971e"}},{"branch":"9.1","label":"v9.1.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.6","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: Tim Sullivan <[email protected]> Co-authored-by: Brandon Kobel <[email protected]>
1 parent 002008a commit 00f077b

File tree

21 files changed

+675
-71
lines changed

21 files changed

+675
-71
lines changed

src/platform/packages/private/kbn-reporting/get_csv_panel_actions/panel_actions/get_csv_panel_action.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,11 @@ export class ReportingCsvPanelAction implements ActionDefinition<EmbeddableApiCo
160160
const license = await firstValueFrom(licensing.license$);
161161
const licenseHasCsvReporting = checkLicense(license.check('reporting', 'basic')).showLinks;
162162

163+
const capabilities = application.capabilities;
163164
// NOTE: For historical reasons capability identifier is called `downloadCsv. It can not be renamed.
164-
const capabilityHasCsvReporting = application.capabilities.dashboard_v2?.downloadCsv === true;
165+
const capabilityHasCsvReporting =
166+
capabilities.dashboard_v2?.downloadCsv === true ||
167+
capabilities.reportingLegacy?.generateReport === true;
165168
if (!licenseHasCsvReporting || !capabilityHasCsvReporting) {
166169
return false;
167170
}

src/platform/packages/private/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ export const reportingCsvShareProvider = ({
6565
const licenseHasCsvReporting = licenseCheck.showLinks;
6666
const licenseDisabled = !licenseCheck.enableLinks;
6767

68-
const capabilityHasCsvReporting = application.capabilities.discover_v2?.generateCsv === true;
68+
const capabilityHasCsvReporting =
69+
application.capabilities.discover_v2?.generateCsv === true ||
70+
application.capabilities.reportingLegacy?.generateReport === true;
6971

7072
const generateReportingJobCSV = ({ intl }: { intl: InjectedIntl }) => {
7173
const { reportType, decoratedJobParams } = getSearchCsvJobParams({

src/platform/packages/private/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import { i18n } from '@kbn/i18n';
1111
import { FormattedMessage } from '@kbn/i18n-react';
1212
import { toMountPoint } from '@kbn/react-kibana-mount';
13+
import type { Capabilities } from '@kbn/core/public';
1314
import { ShareContext, ShareMenuItemV2, ShareMenuProvider } from '@kbn/share-plugin/public';
1415
import React from 'react';
1516
import { firstValueFrom } from 'rxjs';
@@ -39,6 +40,13 @@ const getJobParams = (opts: JobParamsProviderOptions, type: 'pngV2' | 'printable
3940
return { ...baseParams, locatorParams };
4041
};
4142

43+
const hasCapabilityByKey = (capabilities: Capabilities, capabilityKey: keyof Capabilities) => {
44+
return (
45+
capabilities[capabilityKey]?.generateScreenshot === true ||
46+
capabilities.reportingLegacy?.generateReport === true
47+
);
48+
};
49+
4250
/**
4351
* This is used by Dashboard and Visualize apps (sharing modal)
4452
*/
@@ -63,10 +71,14 @@ export const reportingExportModalProvider = ({
6371
const licenseHasScreenshotReporting = showLinks;
6472
const licenseDisabled = !enableLinks;
6573

66-
const capabilityHasDashboardScreenshotReporting =
67-
application.capabilities.dashboard_v2?.generateScreenshot === true;
68-
const capabilityHasVisualizeScreenshotReporting =
69-
application.capabilities.visualize_v2?.generateScreenshot === true;
74+
const capabilityHasDashboardReporting = hasCapabilityByKey(
75+
application.capabilities,
76+
'dashboard_v2'
77+
);
78+
const capabilityHasVisualizeReporting = hasCapabilityByKey(
79+
application.capabilities,
80+
'visualize_v2'
81+
);
7082

7183
if (!licenseHasScreenshotReporting) {
7284
return [];
@@ -78,15 +90,11 @@ export const reportingExportModalProvider = ({
7890
return [];
7991
}
8092

81-
if (objectType === 'dashboard' && !capabilityHasDashboardScreenshotReporting) {
93+
if (objectType === 'dashboard' && !capabilityHasDashboardReporting) {
8294
return [];
8395
}
8496

85-
if (
86-
isSupportedType &&
87-
!capabilityHasVisualizeScreenshotReporting &&
88-
!capabilityHasDashboardScreenshotReporting
89-
) {
97+
if (isSupportedType && !capabilityHasVisualizeReporting && !capabilityHasDashboardReporting) {
9098
return [];
9199
}
92100

x-pack/platform/packages/private/security/authorization_core/src/privileges/privileges.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ export function privilegesFactory(
245245
read: [actions.login, ...readActions],
246246
},
247247
reserved: features.reduce((acc: Record<string, string[]>, feature: KibanaFeature) => {
248+
// Reserved privileges are intentionally not excluded from registration based on their `hidden` attribute.
249+
// This is explicitly to support the legacy reporting use case.
248250
if (feature.reserved) {
249251
feature.reserved.privileges.forEach((reservedPrivilege) => {
250252
acc[reservedPrivilege.id] = [

x-pack/platform/plugins/private/canvas/public/services/kibana_services.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,17 @@ export const setKibanaServices = (
4545
kibanaVersion = initContext.env.packageInfo.version;
4646

4747
coreServices = kibanaCore;
48+
const capabilities = kibanaCore.application.capabilities;
4849
contentManagementService = deps.contentManagement;
4950
dataService = deps.data;
5051
dataViewsService = deps.dataViews;
5152
embeddableService = deps.embeddable;
5253
expressionsService = deps.expressions;
5354
presentationUtilService = deps.presentationUtil;
54-
reportingService = Boolean(kibanaCore.application.capabilities.canvas?.generatePdf)
55+
reportingService = Boolean(
56+
capabilities.canvas?.generatePdf === true ||
57+
capabilities.reportingLegacy?.generateReport === true
58+
)
5559
? deps.reporting
5660
: undefined;
5761
spacesService = deps.spaces;

x-pack/platform/plugins/private/reporting/server/features.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ interface FeatureRegistrationOpts {
1616
}
1717

1818
export function registerFeatures({ isServerless, features }: FeatureRegistrationOpts) {
19-
// Register a 'shell' feature specifically for Serverless. If granted, it will automatically provide access to
20-
// reporting capabilities in other features, such as Discover, Dashboards, and Visualizations. On its own, this
21-
// feature doesn't grant any additional privileges.
19+
// Register a 'shell' features for Reporting. On their own, they don't grant specific privileges.
20+
21+
// Shell feature for Serverless. If granted, it will automatically provide access to
22+
// reporting capabilities in other features, such as Discover, Dashboards, and Visualizations.
2223
if (isServerless) {
2324
features.registerKibanaFeature({
2425
id: 'reporting',
@@ -34,6 +35,44 @@ export function registerFeatures({ isServerless, features }: FeatureRegistration
3435
read: { disabled: true, savedObject: { all: [], read: [] }, ui: [] },
3536
},
3637
});
38+
} else {
39+
// Shell feature for self-managed environments, to be leveraged by a reserved privilege defined
40+
// in ES. This grants access to reporting features in a legacy fashion.
41+
features.registerKibanaFeature({
42+
id: 'reportingLegacy',
43+
name: i18n.translate('xpack.reporting.features.reportingLegacyFeatureName', {
44+
defaultMessage: 'Reporting Legacy',
45+
}),
46+
category: DEFAULT_APP_CATEGORIES.management,
47+
management: { insightsAndAlerting: ['reporting'] },
48+
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
49+
hidden: true,
50+
app: [],
51+
privileges: null,
52+
reserved: {
53+
description: i18n.translate(
54+
'xpack.reporting.features.reportingLegacyFeatureReservedDescription',
55+
{
56+
defaultMessage:
57+
'Reserved for use by the Reporting plugin. This feature is used to grant access to Reporting capabilities in a legacy manner.',
58+
}
59+
),
60+
privileges: [
61+
{
62+
id: 'reporting_user',
63+
privilege: {
64+
excludeFromBasePrivileges: true,
65+
app: [],
66+
catalogue: [],
67+
management: { insightsAndAlerting: ['reporting'] },
68+
savedObject: { all: [], read: [] },
69+
api: ['generateReport'],
70+
ui: ['generateReport'],
71+
},
72+
},
73+
],
74+
},
75+
});
3776
}
3877

3978
features.enableReportingUiCapabilities();

x-pack/platform/plugins/private/reporting/server/plugin.test.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,8 @@
55
* 2.0.
66
*/
77

8-
import {
9-
CoreSetup,
10-
CoreStart,
11-
DEFAULT_APP_CATEGORIES,
12-
Logger,
13-
type PackageInfo,
14-
} from '@kbn/core/server';
8+
import type { CoreSetup, CoreStart, Logger } from '@kbn/core/server';
9+
import { DEFAULT_APP_CATEGORIES, type PackageInfo } from '@kbn/core/server';
1510
import { coreMock, loggingSystemMock } from '@kbn/core/server/mocks';
1611
import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
1712
import { createMockConfigSchema } from '@kbn/reporting-mocks-server';
@@ -25,7 +20,7 @@ import { ReportingPlugin } from './plugin';
2520
import { createMockPluginSetup, createMockPluginStart } from './test_helpers';
2621
import type { ReportingSetupDeps } from './types';
2722
import { ExportTypesRegistry } from '@kbn/reporting-server/export_types_registry';
28-
import { FeaturesPluginSetup } from '@kbn/features-plugin/server';
23+
import type { FeaturesPluginSetup } from '@kbn/features-plugin/server';
2924

3025
const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
3126

@@ -170,7 +165,7 @@ describe('Reporting Plugin', () => {
170165
describe('features registration', () => {
171166
it('does not register Kibana reporting feature in traditional build flavour', async () => {
172167
plugin.setup(coreSetup, pluginSetup);
173-
expect(featuresSetup.registerKibanaFeature).not.toHaveBeenCalled();
168+
expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledTimes(1);
174169
expect(featuresSetup.enableReportingUiCapabilities).toHaveBeenCalledTimes(1);
175170
});
176171

x-pack/platform/plugins/shared/features/common/kibana_feature.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ export interface KibanaFeatureConfig {
159159
* Indicates whether the feature is available as a standalone feature. The feature can still be
160160
* referenced by other features, but it will not be displayed in any feature management UIs. By default, all features
161161
* are visible.
162+
*
163+
* @note This flag is designed for use via configuration overrides, and very select use cases. Please consult prior to use.
162164
*/
163165
hidden?: boolean;
164166

x-pack/platform/plugins/shared/features/server/feature_registry.test.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2030,6 +2030,114 @@ describe('FeatureRegistry', () => {
20302030
);
20312031
});
20322032

2033+
it('allows reserved features to be hidden', () => {
2034+
const feature: KibanaFeatureConfig = {
2035+
id: 'test-feature',
2036+
name: 'Test Feature',
2037+
app: [],
2038+
category: { id: 'foo', label: 'foo' },
2039+
privileges: null,
2040+
hidden: true,
2041+
reserved: {
2042+
description: 'my reserved privileges',
2043+
privileges: [
2044+
{
2045+
id: 'a_reserved_1',
2046+
privilege: {
2047+
savedObject: {
2048+
all: [],
2049+
read: [],
2050+
},
2051+
ui: [],
2052+
app: [],
2053+
},
2054+
},
2055+
],
2056+
},
2057+
};
2058+
2059+
const featureRegistry = new FeatureRegistry();
2060+
expect(() => featureRegistry.registerKibanaFeature(feature)).not.toThrowError();
2061+
});
2062+
2063+
it('does not allow features with both regular and reserved privileges to be hidden', () => {
2064+
const feature: KibanaFeatureConfig = {
2065+
id: 'test-feature',
2066+
name: 'Test Feature',
2067+
app: [],
2068+
category: { id: 'foo', label: 'foo' },
2069+
privileges: {
2070+
all: {
2071+
savedObject: {
2072+
all: [],
2073+
read: [],
2074+
},
2075+
ui: [],
2076+
},
2077+
read: {
2078+
savedObject: {
2079+
all: [],
2080+
read: [],
2081+
},
2082+
ui: [],
2083+
},
2084+
},
2085+
hidden: true,
2086+
reserved: {
2087+
description: 'my reserved privileges',
2088+
privileges: [
2089+
{
2090+
id: 'a_reserved_1',
2091+
privilege: {
2092+
savedObject: {
2093+
all: [],
2094+
read: [],
2095+
},
2096+
ui: [],
2097+
app: [],
2098+
},
2099+
},
2100+
],
2101+
},
2102+
};
2103+
2104+
const featureRegistry = new FeatureRegistry();
2105+
expect(() =>
2106+
featureRegistry.registerKibanaFeature(feature)
2107+
).toThrowErrorMatchingInlineSnapshot(`"Feature test-feature cannot be hidden."`);
2108+
});
2109+
2110+
it('does not allow features with regular privileges to be hidden', () => {
2111+
const feature: KibanaFeatureConfig = {
2112+
id: 'test-feature',
2113+
name: 'Test Feature',
2114+
app: [],
2115+
category: { id: 'foo', label: 'foo' },
2116+
privileges: {
2117+
all: {
2118+
savedObject: {
2119+
all: [],
2120+
read: [],
2121+
},
2122+
ui: [],
2123+
},
2124+
read: {
2125+
savedObject: {
2126+
all: [],
2127+
read: [],
2128+
},
2129+
ui: [],
2130+
},
2131+
},
2132+
hidden: true,
2133+
};
2134+
2135+
const featureRegistry = new FeatureRegistry();
2136+
expect(() =>
2137+
featureRegistry.registerKibanaFeature(feature)
2138+
).toThrowErrorMatchingInlineSnapshot(`"Feature test-feature cannot be hidden."`);
2139+
});
2140+
20332141
it('allows independent sub-feature privileges to register a minimumLicense', () => {
20342142
const feature1: KibanaFeatureConfig = {
20352143
id: 'test-feature',

x-pack/platform/plugins/shared/features/server/feature_schema.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ const kibanaSubFeatureSchema = schema.object({
217217
),
218218
});
219219

220-
// NOTE: This schema intentionally omits the `composedOf` and `hidden` properties to discourage consumers from using
220+
// NOTE: This schema intentionally omits the `composedOf` property to discourage consumers from using
221221
// them during feature registration. This is because these properties should only be set via configuration overrides.
222222
const kibanaFeatureSchema = schema.object({
223223
id: schema.string({
@@ -233,6 +233,9 @@ const kibanaFeatureSchema = schema.object({
233233
name: schema.string(),
234234
category: appCategorySchema,
235235
scope: schema.maybe(schema.arrayOf(schema.string(), { minSize: 1 })),
236+
// The hidden flag is only supported for explicit configuration for features with reserved privileges.
237+
// All other usages are via configuration overrides.
238+
hidden: schema.maybe(schema.boolean()),
236239
description: schema.maybe(schema.string()),
237240
order: schema.maybe(schema.number()),
238241
excludeFromBasePrivileges: schema.maybe(schema.boolean()),
@@ -322,6 +325,14 @@ const elasticsearchFeatureSchema = schema.object({
322325
export function validateKibanaFeature(feature: KibanaFeatureConfig) {
323326
kibanaFeatureSchema.validate(feature);
324327

328+
// The `hidden` attribute is ONLY permitted for features with reserved privileges, AND without normal privileges.
329+
// The original intent of the hidden flag was to support serverless configuration overrides. There is now (>=9.0) an additional,
330+
// maybe temporary use case for the legacy reporting authorization mode, which registers as a reserved privilege.
331+
const { hidden, privileges, reserved } = feature;
332+
if (hidden && (privileges !== null || typeof reserved === 'undefined')) {
333+
throw new Error(`Feature ${feature.id} cannot be hidden.`);
334+
}
335+
325336
const unknownScopesEntries = difference(feature.scope ?? [], Object.values(KibanaFeatureScope));
326337

327338
if (unknownScopesEntries.length) {

0 commit comments

Comments
 (0)