Skip to content

Commit 82eb4ff

Browse files
committed
Add reporting_user feature for reserved set of privileges (elastic#231533)
## Summary We want to switch the reserved `reporting_user` role to use a "reserved privilege definition" and uses just that privilege. This PR satisfies the Kibana requirements. There is a corresponding Elasticsearch PR: elastic/elasticsearch#132766 ## Testing **NOTE: PNG/PDF reporting requires a Trial, or Gold+ license** 1. Create `test_reporting_user` role ``` POST /_security/role/test_reporting_user { "cluster": [], "indices": [], "application": [{ "application": "kibana-*", "privileges": ["reserved_reporting_user"], "resources": ["*"] }] } ``` 2. Create `test_analyst_user` role ``` POST /_security/role/test_analyst_user { "cluster": [], "indices": [ { "names": ["kibana_sample_*"], "privileges": ["all"], "field_security": { "grant": ["*"], "except": [] }, "allow_restricted_indices": false } ], "applications": [ { "application": "kibana-.kibana", "privileges": [ "feature_discover_v2.read", "feature_dashboard_v2.read", "feature_canvas.read", "feature_visualize_v2.read" ], "resources": ["space:default"] } ], "run_as": [], "metadata": {}, "transient_metadata": { "enabled": true } } ``` 3. Create a test user with just those two roles. Install sample data. Log in using the new test user. 4. Test cases | App | Reporting feature |-|- | Dashboard | PDF, PNG, CSV (from saved search panel action) | Discover | CSV | Canvas | PDF | Lens | PDF, PNG | Stack Management | List reports, download reports, view report info, delete reports 6. As admin, create an additional Space which the test user should not have access to. Ensure the test user does not have access to those spaces. 7. Remove the `test_reporting_user` role from the user and ensure they do not see any Reporting controls in the UI, and can not access Stack Management > Reporting. ## Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - ~~[ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)~~ - ~~[ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - ~~[ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)~~ - ~~[ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations.~~ - ~~[ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed~~ - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. --------- Co-authored-by: Larry Gregory <[email protected]> (cherry picked from commit f9be58b) # Conflicts: # src/platform/packages/private/kbn-reporting/public/share/share_context_menu/register_csv_modal_reporting.tsx # src/platform/packages/private/kbn-reporting/public/share/share_context_menu/register_pdf_png_modal_reporting.tsx # src/platform/test/functional/page_objects/export_page.ts # x-pack/platform/plugins/private/reporting/server/plugin.test.ts # x-pack/test/api_integration/apis/features/features/features.ts # x-pack/test/reporting_api_integration/reporting_and_security/default_reporting_user_role.ts # x-pack/test/reporting_api_integration/services/scenarios.ts # x-pack/test/reporting_functional/services/scenarios.ts
1 parent 4fc05d4 commit 82eb4ff

File tree

19 files changed

+653
-59
lines changed

19 files changed

+653
-59
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
}

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: 3 additions & 8 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

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

x-pack/platform/plugins/shared/spaces/public/management/components/enabled_features/__snapshots__/enabled_features.test.tsx.snap

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/platform/plugins/shared/spaces/public/management/components/enabled_features/enabled_features.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ const features: KibanaFeatureConfig[] = [
3232
category: DEFAULT_APP_CATEGORIES.kibana,
3333
privileges: null,
3434
},
35+
{
36+
id: 'feature-3',
37+
name: 'Feature 3 (hidden)',
38+
hidden: true,
39+
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
40+
app: [],
41+
category: DEFAULT_APP_CATEGORIES.kibana,
42+
privileges: null,
43+
},
3544
];
3645

3746
describe('EnabledFeatures', () => {

0 commit comments

Comments
 (0)