diff --git a/test/functional/page_objects/common_page.js b/test/functional/page_objects/common_page.js index b6ccd621570ec..b16d0d04531a5 100644 --- a/test/functional/page_objects/common_page.js +++ b/test/functional/page_objects/common_page.js @@ -357,6 +357,11 @@ export function CommonPageProvider({ getService, getPageObjects }) { } } } + + async getBodyText() { + const el = await find.byCssSelector('body>pre'); + return await el.getVisibleText(); + } } return new CommonPage(); diff --git a/test/functional/page_objects/error_page.js b/test/functional/page_objects/error_page.js index 0740f464209c8..dd4966ca690fd 100644 --- a/test/functional/page_objects/error_page.js +++ b/test/functional/page_objects/error_page.js @@ -18,13 +18,22 @@ */ import expect from '@kbn/expect'; -export function ErrorPageProvider({ getService }) { - const find = getService('find'); +export function ErrorPageProvider({ getPageObjects }) { + const PageObjects = getPageObjects(['common']); class ErrorPage { + async expectForbidden() { + const messageText = await PageObjects.common.getBodyText(); + expect(messageText).to.eql( + JSON.stringify({ + statusCode: 403, + error: 'Forbidden', + message: 'Forbidden' + }) + ); + } async expectNotFound() { - const el = await find.byCssSelector('body>pre'); - const messageText = await el.getVisibleText(); + const messageText = await PageObjects.common.getBodyText(); expect(messageText).to.eql( JSON.stringify({ statusCode: 404, diff --git a/x-pack/plugins/ml/index.js b/x-pack/plugins/ml/index.js index ea2bb630e8efe..7bdca896d2999 100644 --- a/x-pack/plugins/ml/index.js +++ b/x-pack/plugins/ml/index.js @@ -88,20 +88,19 @@ export const ml = (kibana) => { navLinkId: 'ml', app: ['ml', 'kibana'], catalogue: ['ml'], - privileges: { - all: { - catalogue: ['ml'], - grantWithBaseRead: true, + privileges: {}, + reserved: { + privilege: { savedObject: { all: [], read: ['config'] }, ui: [], }, - }, - privilegesTooltip: i18n.translate('xpack.ml.privileges.tooltip', { - defaultMessage: 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.' - }) + description: i18n.translate('xpack.ml.feature.reserved.description', { + defaultMessage: 'To grant users access, you should also assign either the machine_learning_user or machine_learning_admin role.' + }) + } }); // Add server routes and initialize the plugin here diff --git a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js index ce94e47203b2a..76fd0675fb415 100644 --- a/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js +++ b/x-pack/plugins/ml/public/jobs/jobs_list/components/jobs_list_view/jobs_list_view.js @@ -406,7 +406,7 @@ export class JobsListView extends Component { -
+
diff --git a/x-pack/plugins/monitoring/init.js b/x-pack/plugins/monitoring/init.js index d234998b30d50..358fd715d7df6 100644 --- a/x-pack/plugins/monitoring/init.js +++ b/x-pack/plugins/monitoring/init.js @@ -64,20 +64,19 @@ export const init = (monitoringPlugin, server) => { navLinkId: 'monitoring', app: ['monitoring', 'kibana'], catalogue: ['monitoring'], - privileges: { - all: { - catalogue: ['monitoring'], - grantWithBaseRead: true, + privileges: {}, + reserved: { + privilege: { savedObject: { all: [], - read: ['config'], + read: ['config'] }, ui: [], }, - }, - privilegesTooltip: i18n.translate('xpack.monitoring.privileges.tooltip', { - defaultMessage: 'To grant users access, you should also assign the monitoring_user role.' - }) + description: i18n.translate('xpack.monitoring.feature.reserved.description', { + defaultMessage: 'To grant users access, you should also assign the monitoring_user role.' + }) + } }); const bulkUploader = initBulkUploader(kbnServer, server); diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index 5fb316b772508..bca0684209ba1 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -7,3 +7,5 @@ export const GLOBAL_RESOURCE = '*'; export const IGNORED_TYPES = ['space']; export const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; +export const APPLICATION_PREFIX = 'kibana-'; +export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; diff --git a/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts index f7fa66813ee8c..fd4cdf33028eb 100644 --- a/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts +++ b/x-pack/plugins/security/common/model/kibana_privileges/feature_privileges.ts @@ -19,7 +19,12 @@ export class KibanaFeaturePrivileges { } public getPrivileges(featureId: string): string[] { - return Object.keys(this.featurePrivilegesMap[featureId]); + const featurePrivileges = this.featurePrivilegesMap[featureId]; + if (featurePrivileges == null) { + return []; + } + + return Object.keys(featurePrivileges); } public getActions(featureId: string, privilege: string): string[] { diff --git a/x-pack/plugins/security/common/model/raw_kibana_privileges.ts b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts index bdef00e8f7c21..1b1584a4ce58c 100644 --- a/x-pack/plugins/security/common/model/raw_kibana_privileges.ts +++ b/x-pack/plugins/security/common/model/raw_kibana_privileges.ts @@ -14,4 +14,5 @@ export interface RawKibanaPrivileges { global: Record; features: RawKibanaFeaturePrivileges; space: Record; + reserved: Record; } diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 516fdb199b8c3..19bea7ccdfefb 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -19,6 +19,7 @@ export interface RoleKibanaPrivilege { spaces: string[]; base: string[]; feature: FeaturesPrivileges; + _reserved?: string[]; } export interface Role { diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts index d95e20c476fbe..d598a9da67a51 100644 --- a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/__fixtures__/default_privilege_definition.ts @@ -34,4 +34,5 @@ export const defaultPrivilegeDefinition = new KibanaPrivileges({ all: ['ui:/feature3/foo', 'ui:/feature3/foo/*'], }, }, + reserved: {}, }); diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts index bd8358b84e232..f2e2c4bc1be99 100644 --- a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_allowed_privileges_calculator.ts @@ -93,6 +93,10 @@ export class KibanaAllowedPrivilegesCalculator { candidateFeaturePrivileges: string[] ): { privileges: string[]; canUnassign: boolean } { const effectiveFeaturePrivilegeExplanation = effectivePrivileges.feature[featureId]; + if (effectiveFeaturePrivilegeExplanation == null) { + throw new Error('To calculate allowed feature privileges, we need the effective privileges'); + } + const effectiveFeatureActions = this.getFeatureActions( featureId, effectiveFeaturePrivilegeExplanation.actualPrivilege diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts index dd6f3414958b9..58c371e80290b 100644 --- a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator.ts @@ -76,6 +76,7 @@ export class KibanaPrivilegeCalculator { ignoreAssigned ), feature: {}, + reserved: privilegeSpec._reserved, }; // If calculations wish to ignoreAssigned, then we still need to know what the real effective base privilege is diff --git a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts index 65dd9248c32bd..41f6012737a92 100644 --- a/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts +++ b/x-pack/plugins/security/public/lib/kibana_privilege_calculator/kibana_privilege_calculator_types.ts @@ -32,8 +32,9 @@ export interface PrivilegeExplanation { export interface CalculatedPrivilege { base: PrivilegeExplanation; feature: { - [featureId: string]: PrivilegeExplanation; + [featureId: string]: PrivilegeExplanation | undefined; }; + reserved: undefined | string[]; } export interface PrivilegeScenario { @@ -50,9 +51,11 @@ export interface AllowedPrivilege { canUnassign: boolean; }; feature: { - [featureId: string]: { - privileges: string[]; - canUnassign: boolean; - }; + [featureId: string]: + | { + privileges: string[]; + canUnassign: boolean; + } + | undefined; }; } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap index 599c4f1c9e193..617335dc9fb34 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap @@ -13,6 +13,7 @@ exports[` renders without crashing 1`] = ` "rawKibanaPrivileges": Object { "features": Object {}, "global": Object {}, + "reserved": Object {}, "space": Object {}, }, } @@ -24,6 +25,7 @@ exports[` renders without crashing 1`] = ` "rawKibanaPrivileges": Object { "features": Object {}, "global": Object {}, + "reserved": Object {}, "space": Object {}, }, }, diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx index 063c561c9cf48..9648bf1d111bf 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx @@ -36,6 +36,7 @@ const defaultPrivilegeDefinition = new KibanaPrivileges({ all: ['somethingObsecure:/foo'], }, }, + reserved: {}, }); interface BuildRoleOpts { diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx index 3041b47bd3ad3..0dae2cda807a0 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx @@ -61,14 +61,32 @@ export class FeatureTable extends Component { public render() { const { role, features, calculatedPrivileges, rankedFeaturePrivileges } = this.props; - const items: TableRow[] = features.map(feature => ({ - feature: { - ...feature, - hasAnyPrivilegeAssigned: - calculatedPrivileges.feature[feature.id].actualPrivilege !== NO_PRIVILEGE_VALUE, - }, - role, - })); + const items: TableRow[] = features + .sort((feature1, feature2) => { + if (feature1.reserved && !feature2.reserved) { + return 1; + } + + if (feature2.reserved && !feature1.reserved) { + return -1; + } + + return 0; + }) + .map(feature => { + const calculatedFeaturePrivileges = calculatedPrivileges.feature[feature.id]; + const hasAnyPrivilegeAssigned = Boolean( + calculatedFeaturePrivileges && + calculatedFeaturePrivileges.actualPrivilege !== NO_PRIVILEGE_VALUE + ); + return { + feature: { + ...feature, + hasAnyPrivilegeAssigned, + }, + role, + }; + }); // TODO: This simply grabs the available privileges from the first feature we encounter. // As of now, features can have 'all' and 'read' as available privileges. Once that assumption breaks, @@ -147,13 +165,17 @@ export class FeatureTable extends Component { ), render: (roleEntry: Role, record: TableRow) => { - const featureId = record.feature.id; + const { id: featureId, reserved } = record.feature; + + if (reserved) { + return {reserved.description}; + } const featurePrivileges = this.props.kibanaPrivileges .getFeaturePrivileges() .getPrivileges(featureId); - if (!featurePrivileges) { + if (featurePrivileges.length === 0) { return null; } @@ -213,18 +235,32 @@ export class FeatureTable extends Component { return featurePrivileges; } - return allowedPrivileges.feature[featureId].privileges; + const allowedFeaturePrivileges = allowedPrivileges.feature[featureId]; + if (allowedFeaturePrivileges == null) { + throw new Error('Unable to get enabled feature privileges for a feature without privileges'); + } + + return allowedFeaturePrivileges.privileges; }; private getPrivilegeExplanation = (featureId: string): PrivilegeExplanation => { const { calculatedPrivileges } = this.props; + const calculatedFeaturePrivileges = calculatedPrivileges.feature[featureId]; + if (calculatedFeaturePrivileges == null) { + throw new Error('Unable to get privilege explanation for a feature without privileges'); + } - return calculatedPrivileges.feature[featureId]; + return calculatedFeaturePrivileges; }; private allowsNoneForPrivilegeAssignment = (featureId: string): boolean => { const { allowedPrivileges } = this.props; - return allowedPrivileges.feature[featureId].canUnassign; + const allowedFeaturePrivileges = allowedPrivileges.feature[featureId]; + if (allowedFeaturePrivileges == null) { + throw new Error('Unable to determine if none is allowed for a feature without privileges'); + } + + return allowedFeaturePrivileges.canUnassign; }; private onChangeAllFeaturePrivileges = (privilege: string) => { diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.test.tsx index da2514c6088aa..bcbec6575b0d9 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges_region.test.tsx @@ -43,6 +43,7 @@ const buildProps = (customProps = {}) => { global: {}, space: {}, features: {}, + reserved: {}, }), intl: null as any, uiCapabilities: { diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index c75e2044b03db..c12f23e6a6d32 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ // @ts-ignore -import { EuiButtonGroup, EuiButtonGroupProps, EuiSuperSelect } from '@elastic/eui'; +import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { Feature } from '../../../../../../../../../xpack_main/types'; @@ -23,6 +23,7 @@ const buildProps = (customProps: any = {}) => { }, global: {}, space: {}, + reserved: {}, }); const role = { @@ -130,6 +131,29 @@ describe('', () => { expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); }); + it('displays the reserved privilege', () => { + const props = buildProps({ + role: { + elasticsearch: {}, + kibana: [ + { + spaces: ['*'], + base: [], + feature: {}, + _reserved: ['foo'], + }, + ], + }, + }); + const wrapper = shallowWithIntl(); + const selector = wrapper.find(EuiComboBox); + expect(selector.props()).toMatchObject({ + isDisabled: true, + selectedOptions: [{ label: 'foo' }], + }); + expect(wrapper.find(UnsupportedSpacePrivilegesWarning)).toHaveLength(0); + }); + it('fires its onChange callback when the privilege changes', () => { const props = buildProps(); const wrapper = mountWithIntl(); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx index 27cf088385277..e1123284c513d 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -5,6 +5,7 @@ */ import { + EuiComboBox, // @ts-ignore EuiDescribedFormGroup, EuiFormRow, @@ -14,8 +15,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; + import { Feature } from '../../../../../../../../../xpack_main/types'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; import { copyRole } from '../../../../../../../lib/role_utils'; @@ -65,6 +67,11 @@ export class SimplePrivilegeSection extends Component { this.state.globalPrivsIndex ]; + const hasReservedPrivileges = + calculatedPrivileges && + calculatedPrivileges.reserved != null && + calculatedPrivileges.reserved.length > 0; + const description = (

{ description={description} > - - - - ), - dropdownDisplay: ( - - + {hasReservedPrivileges ? ( + ({ + label: privilege, + }))} + isDisabled + /> + ) : ( + - -

+ + ), + dropdownDisplay: ( + + + + +

+ +

+ + ), + }, + { + value: CUSTOM_PRIVILEGE_VALUE, + inputDisplay: ( + -

-
- ), - }, - { - value: CUSTOM_PRIVILEGE_VALUE, - inputDisplay: ( - - - - ), - dropdownDisplay: ( - - - - -

- -

-
- ), - }, - { - value: 'read', - inputDisplay: ( - - - - ), - dropdownDisplay: ( - - + + ), + dropdownDisplay: ( + + + + +

+ +

+
+ ), + }, + { + value: 'read', + inputDisplay: ( + -
-

- -

- - ), - }, - { - value: 'all', - inputDisplay: ( - - - - ), - dropdownDisplay: ( - - + + ), + dropdownDisplay: ( + + + + +

+ +

+
+ ), + }, + { + value: 'all', + inputDisplay: ( + -
-

- -

- - ), - }, - ]} - hasDividers - valueOfSelected={kibanaPrivilege} - /> + + ), + dropdownDisplay: ( + + + + +

+ +

+
+ ), + }, + ]} + hasDividers + valueOfSelected={kibanaPrivilege} + /> + )} {this.state.isCustomizingGlobalPrivilege && ( @@ -312,7 +329,7 @@ export class SimplePrivilegeSection extends Component { return spacePrivileges.find(privileges => isGlobalPrivilegeDefinition(privileges)); }; - private createGlobalPrivilegeEntry(role: Role) { + private createGlobalPrivilegeEntry(role: Role): RoleKibanaPrivilege { const newEntry = { spaces: ['*'], base: [], diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index c0eea3ff9424c..a04f277c8aa35 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -321,6 +321,7 @@ exports[` renders without crashing 1`] = ` "isDirectlyAssigned": true, }, "feature": Object {}, + "reserved": undefined, } } disabled={true} @@ -432,6 +433,7 @@ exports[` renders without crashing 1`] = ` "rawKibanaPrivileges": Object { "features": Object {}, "global": Object {}, + "reserved": Object {}, "space": Object {}, }, } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 5c2b8662d326c..d8bb01b01af82 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -14,7 +14,7 @@ import { import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; interface Props extends EuiTextProps { - privilege: string | string[]; + privilege: string | string[] | undefined; explanation?: PrivilegeExplanation; iconType?: IconType; tooltipContent?: ReactNode; @@ -90,7 +90,7 @@ PrivilegeDisplay.defaultProps = { privilege: [], }; -function getDisplayValue(privilege: string | string[]) { +function getDisplayValue(privilege: string | string[] | undefined) { const privileges = coerceToArray(privilege); let displayValue: string | ReactNode; @@ -125,7 +125,10 @@ function getIconTip(iconType?: IconType, tooltipContent?: ReactNode) { ); } -function coerceToArray(privilege: string | string[]): string[] { +function coerceToArray(privilege: string | string[] | undefined): string[] { + if (privilege === undefined) { + return []; + } if (Array.isArray(privilege)) { return privilege; } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx index 71fe2b1d671cf..fb8a67144aa8e 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx @@ -103,6 +103,7 @@ describe('PrivilegeMatrix', () => { all: [], read: [], }, + reserved: {}, }) ).getInstance(role); diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index 0f6918ba1e0b7..93caf2e477d21 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -288,11 +288,14 @@ export class PrivilegeMatrix extends Component { return ; } - const actualPrivileges = this.props.calculatedPrivileges[column.spacesIndex].feature[ - feature.id - ].actualPrivilege; + const featureCalculatedPrivilege = this.props.calculatedPrivileges[column.spacesIndex] + .feature[feature.id]; - return ; + return ( + + ); } else { // not global @@ -315,7 +318,7 @@ export class PrivilegeMatrix extends Component { return ( ); } diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index a57b97dcd9e4b..3ef291eafe690 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -33,12 +33,14 @@ const buildProps = (customProps = {}) => { features: {}, global: {}, space: {}, + reserved: {}, }), privilegeCalculatorFactory: new KibanaPrivilegeCalculatorFactory( new KibanaPrivileges({ global: {}, features: {}, space: {}, + reserved: {}, }) ), features: [], diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index de9183d840f69..1d086c6dfb519 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -511,7 +511,7 @@ export class PrivilegeSpaceForm extends Component { const featureEntries = Object.values(allowedPrivileges.feature); return featureEntries.some(entry => { - return entry.canUnassign || entry.privileges.length > 1; + return entry != null && (entry.canUnassign || entry.privileges.length > 1); }); }; @@ -541,9 +541,9 @@ export class PrivilegeSpaceForm extends Component { form.feature = {}; } else { this.props.features.forEach(feature => { - const canAssign = allowedPrivs[this.state.editingIndex].feature[ - feature.id - ].privileges.includes(privileges[0]); + const allowedPrivilegesFeature = allowedPrivs[this.state.editingIndex].feature[feature.id]; + const canAssign = + allowedPrivilegesFeature && allowedPrivilegesFeature.privileges.includes(privileges[0]); if (canAssign) { form.feature[feature.id] = [...privileges]; diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 24afb2d31d43a..1c8e5d3dcc536 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -184,14 +184,17 @@ export class PrivilegeSpaceTable extends Component { name: 'Privileges', render: (privileges: RoleKibanaPrivilege, record: TableRow) => { const hasCustomizations = hasAssignedFeaturePrivileges(privileges); - const basePrivilege = effectivePrivileges[record.spacesIndex].base; + const effectivePrivilege = effectivePrivileges[record.spacesIndex]; + const basePrivilege = effectivePrivilege.base; const isAllowedCustomizations = allowedPrivileges[record.spacesIndex].base.privileges.length > 1; const showCustomize = hasCustomizations && isAllowedCustomizations; - if (record.isGlobal) { + if (effectivePrivilege.reserved != null && effectivePrivilege.reserved.length > 0) { + return ; + } else if (record.isGlobal) { return ( { }, global: {}, space: {}, + reserved: {}, }) ), ...customProps, @@ -117,6 +119,26 @@ describe('', () => { expect(wrapper.find(PrivilegeSpaceForm)).toHaveLength(1); }); + it('hides privilege matrix when the role is reserved', () => { + const props = buildProps({ + role: { + name: '', + metadata: { + _reserved: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + }, + }); + + const wrapper = mountWithIntl(); + expect(wrapper.find(PrivilegeMatrix)).toHaveLength(0); + }); + describe('with base privilege set to "read"', () => { it('allows space privileges to be customized', () => { const props = buildProps({ diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index e80c420797b99..3ce7b0f72aee3 100644 --- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -19,6 +19,7 @@ import { Space } from '../../../../../../../../../spaces/common/model/space'; import { Feature } from '../../../../../../../../../xpack_main/types'; import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { isReservedRole } from '../../../../../../../lib/role_utils'; import { RoleValidator } from '../../../../lib/validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; @@ -219,7 +220,9 @@ class SpaceAwarePrivilegeSectionUI extends Component { return ( {addPrivilegeButton} - {hasPrivilegesAssigned && {viewMatrixButton}} + {hasPrivilegesAssigned && !isReservedRole(this.props.role) && ( + {viewMatrixButton} + )} ); }; diff --git a/x-pack/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap b/x-pack/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap index dfb2ad87c3162..ea7540f2a082e 100644 --- a/x-pack/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap +++ b/x-pack/plugins/security/server/lib/authorization/__snapshots__/privilege_serializer.test.ts.snap @@ -12,6 +12,8 @@ exports[`#deserializeFeaturePrivilege throws error when deserializing foo_featur exports[`#deserializeGlobalBasePrivilege throws Error if isn't a base privilege 1`] = `"Unrecognized global base privilege"`; +exports[`#deserializeReservedPrivilege throws Error if doesn't start with reserved_ 1`] = `"Unrecognized reserved privilege"`; + exports[`#deserializeSpaceBasePrivilege throws Error if prefixed with space_ but not a reserved privilege 1`] = `"Unrecognized space base privilege"`; exports[`#deserializeSpaceBasePrivilege throws Error if provided 'all' 1`] = `"Unrecognized space base privilege"`; diff --git a/x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts b/x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts index 47177b3ed5b60..ecfe0d34fdbcb 100644 --- a/x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/privilege_serializer.test.ts @@ -29,13 +29,52 @@ describe(`#isSerializedSpaceBasePrivilege`, () => { }); }); - ['all', 'read', 'foo', 'bar', 'feature_foo', 'feature_foo.privilege1'].forEach(validValue => { + ['all', 'read', 'foo', 'bar', 'feature_foo', 'feature_foo.privilege1'].forEach(invalid => { + test(`returns false for '${invalid}'`, () => { + expect(PrivilegeSerializer.isSerializedSpaceBasePrivilege(invalid)).toBe(false); + }); + }); +}); + +describe(`#isSerializedReservedPrivilege`, () => { + ['reserved_foo', 'reserved_bar'].forEach(validValue => { test(`returns true for '${validValue}'`, () => { - expect(PrivilegeSerializer.isSerializedSpaceBasePrivilege(validValue)).toBe(false); + expect(PrivilegeSerializer.isSerializedReservedPrivilege(validValue)).toBe(true); + }); + }); + + [ + 'all', + 'read', + 'space_all', + 'space_reserved', + 'foo_reserved', + 'bar', + 'feature_foo', + 'feature_foo.privilege1', + ].forEach(invalidValue => { + test(`returns false for '${invalidValue}'`, () => { + expect(PrivilegeSerializer.isSerializedReservedPrivilege(invalidValue)).toBe(false); }); }); }); +describe(`#isSerializedFeaturePrivilege`, () => { + ['feature_foo.privilege1', 'feature_bar.privilege2'].forEach(validValue => { + test(`returns true for '${validValue}'`, () => { + expect(PrivilegeSerializer.isSerializedFeaturePrivilege(validValue)).toBe(true); + }); + }); + + ['all', 'read', 'space_all', 'space_read', 'reserved_foo', 'reserved_bar'].forEach( + invalidValue => { + test(`returns false for '${invalidValue}'`, () => { + expect(PrivilegeSerializer.isSerializedFeaturePrivilege(invalidValue)).toBe(false); + }); + } + ); +}); + describe('#serializeGlobalBasePrivilege', () => { test('throws Error if unrecognized privilege used', () => { expect(() => @@ -79,6 +118,13 @@ describe('#serializeFeaturePrivilege', () => { }); }); +describe('#serializeReservedPrivilege', () => { + test('returns `reserved_${privilegeName}`', () => { + const result = PrivilegeSerializer.serializeReservedPrivilege('foo'); + expect(result).toBe('reserved_foo'); + }); +}); + describe('#deserializeFeaturePrivilege', () => { [ { @@ -158,3 +204,16 @@ describe('#deserializeSpaceBasePrivilege', () => { expect(result).toBe('read'); }); }); + +describe('#deserializeReservedPrivilege', () => { + test(`throws Error if doesn't start with reserved_`, () => { + expect(() => + PrivilegeSerializer.deserializeReservedPrivilege('all') + ).toThrowErrorMatchingSnapshot(); + }); + + test(`returns 'customApplication1' unprefixed if provided 'reserved_customApplication1'`, () => { + const result = PrivilegeSerializer.deserializeReservedPrivilege('reserved_customApplication1'); + expect(result).toBe('customApplication1'); + }); +}); diff --git a/x-pack/plugins/security/server/lib/authorization/privilege_serializer.ts b/x-pack/plugins/security/server/lib/authorization/privilege_serializer.ts index f27276902c94e..2bbebaa1cc951 100644 --- a/x-pack/plugins/security/server/lib/authorization/privilege_serializer.ts +++ b/x-pack/plugins/security/server/lib/authorization/privilege_serializer.ts @@ -6,6 +6,7 @@ const featurePrefix = 'feature_'; const spacePrefix = 'space_'; +const reservedPrefix = 'reserved_'; const basePrivilegeNames = ['all', 'read']; const globalBasePrivileges = [...basePrivilegeNames]; const spaceBasePrivileges = basePrivilegeNames.map( @@ -29,8 +30,16 @@ export class PrivilegeSerializer { return spaceBasePrivileges.includes(privilegeName); } + public static isSerializedReservedPrivilege(privilegeName: string) { + return privilegeName.startsWith(reservedPrefix); + } + + public static isSerializedFeaturePrivilege(privilegeName: string) { + return privilegeName.startsWith(featurePrefix); + } + public static serializeGlobalBasePrivilege(privilegeName: string) { - if (!basePrivilegeNames.includes(privilegeName)) { + if (!globalBasePrivileges.includes(privilegeName)) { throw new Error('Unrecognized global base privilege'); } @@ -49,6 +58,10 @@ export class PrivilegeSerializer { return `${featurePrefix}${featureId}.${privilegeName}`; } + public static serializeReservedPrivilege(privilegeName: string) { + return `${reservedPrefix}${privilegeName}`; + } + public static deserializeFeaturePrivilege(privilege: string): FeaturePrivilege { const match = privilege.match(deserializeFeaturePrivilegeRegexp); if (!match) { @@ -76,4 +89,12 @@ export class PrivilegeSerializer { return privilege.slice(spacePrefix.length); } + + public static deserializeReservedPrivilege(privilege: string) { + if (!PrivilegeSerializer.isSerializedReservedPrivilege(privilege)) { + throw new Error('Unrecognized reserved privilege'); + } + + return privilege.slice(reservedPrefix.length); + } } diff --git a/x-pack/plugins/security/server/lib/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/lib/authorization/privileges/privileges.test.ts index 62ece7011f166..6a7972334da19 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/privileges/privileges.test.ts @@ -254,7 +254,7 @@ describe('features', () => { }); }); - test(`features with no privileges are specified with an empty object`, () => { + test(`features with no privileges aren't listed`, () => { const features: Feature[] = [ { id: 'foo', @@ -272,7 +272,7 @@ describe('features', () => { const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); const actual = privileges.get(); - expect(actual).toHaveProperty('features.foo', {}); + expect(actual).not.toHaveProperty('features.foo'); }); }); @@ -708,5 +708,234 @@ describe('features', () => { actions.ui.get('foo', 'read-ui-2'), ]); }); + + test('actions defined in a reserved privilege are not included in `all` or `read`', () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + icon: 'arrowDown', + navLinkId: 'kibana:foo', + app: [], + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], + }, + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: ['ignore-me-1', 'ignore-me-2'], + read: ['ignore-me-1', 'ignore-me-2'], + }, + ui: ['ignore-me-1'], + }, + description: '', + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).toHaveProperty(`${group}.all`, [ + actions.login, + actions.version, + ...(expectManageSpaces ? [actions.space.manage, actions.ui.get('spaces', 'manage')] : []), + actions.allHack, + ]); + expect(actual).toHaveProperty(`${group}.read`, [actions.login, actions.version]); + }); + }); +}); + +describe('reserved', () => { + test('actions defined at the feature cascade to the privileges', () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + icon: 'arrowDown', + navLinkId: 'kibana:foo', + app: ['app-1', 'app-2'], + catalogue: ['catalogue-1', 'catalogue-2'], + management: { + foo: ['management-1', 'management-2'], + }, + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + description: '', + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).toHaveProperty('reserved.foo', [ + actions.version, + actions.app.get('app-1'), + actions.app.get('app-2'), + actions.ui.get('catalogue', 'catalogue-1'), + actions.ui.get('catalogue', 'catalogue-2'), + actions.ui.get('management', 'foo', 'management-1'), + actions.ui.get('management', 'foo', 'management-2'), + actions.ui.get('navLinks', 'kibana:foo'), + ]); + }); + + test('actions defined at the reservedPrivilege take precedence', () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + icon: 'arrowDown', + app: ['ignore-me-1', 'ignore-me-2'], + catalogue: ['ignore-me-1', 'ignore-me-2'], + management: { + foo: ['ignore-me-1', 'ignore-me-2'], + }, + privileges: {}, + reserved: { + privilege: { + app: ['app-1', 'app-2'], + catalogue: ['catalogue-1', 'catalogue-2'], + management: { + bar: ['management-1', 'management-2'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + description: '', + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).toHaveProperty('reserved.foo', [ + actions.version, + actions.app.get('app-1'), + actions.app.get('app-2'), + actions.ui.get('catalogue', 'catalogue-1'), + actions.ui.get('catalogue', 'catalogue-2'), + actions.ui.get('management', 'bar', 'management-1'), + actions.ui.get('management', 'bar', 'management-2'), + ]); + }); + + test(`actions only specified at the privilege are alright too`, () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + icon: 'arrowDown', + app: [], + privileges: {}, + reserved: { + privilege: { + savedObject: { + all: ['savedObject-all-1', 'savedObject-all-2'], + read: ['savedObject-read-1', 'savedObject-read-2'], + }, + ui: ['ui-1', 'ui-2'], + }, + description: '', + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).toHaveProperty('reserved.foo', [ + actions.version, + actions.savedObject.get('savedObject-all-1', 'bulk_get'), + actions.savedObject.get('savedObject-all-1', 'get'), + actions.savedObject.get('savedObject-all-1', 'find'), + actions.savedObject.get('savedObject-all-1', 'create'), + actions.savedObject.get('savedObject-all-1', 'bulk_create'), + actions.savedObject.get('savedObject-all-1', 'update'), + actions.savedObject.get('savedObject-all-1', 'delete'), + actions.savedObject.get('savedObject-all-2', 'bulk_get'), + actions.savedObject.get('savedObject-all-2', 'get'), + actions.savedObject.get('savedObject-all-2', 'find'), + actions.savedObject.get('savedObject-all-2', 'create'), + actions.savedObject.get('savedObject-all-2', 'bulk_create'), + actions.savedObject.get('savedObject-all-2', 'update'), + actions.savedObject.get('savedObject-all-2', 'delete'), + actions.savedObject.get('savedObject-read-1', 'bulk_get'), + actions.savedObject.get('savedObject-read-1', 'get'), + actions.savedObject.get('savedObject-read-1', 'find'), + actions.savedObject.get('savedObject-read-2', 'bulk_get'), + actions.savedObject.get('savedObject-read-2', 'get'), + actions.savedObject.get('savedObject-read-2', 'find'), + actions.ui.get('savedObjectsManagement', 'savedObject-all-1', 'delete'), + actions.ui.get('savedObjectsManagement', 'savedObject-all-1', 'edit'), + actions.ui.get('savedObjectsManagement', 'savedObject-all-1', 'read'), + actions.ui.get('savedObjectsManagement', 'savedObject-all-2', 'delete'), + actions.ui.get('savedObjectsManagement', 'savedObject-all-2', 'edit'), + actions.ui.get('savedObjectsManagement', 'savedObject-all-2', 'read'), + actions.ui.get('savedObjectsManagement', 'savedObject-read-1', 'read'), + actions.ui.get('savedObjectsManagement', 'savedObject-read-2', 'read'), + actions.ui.get('foo', 'ui-1'), + actions.ui.get('foo', 'ui-2'), + ]); + }); + + test(`features with no reservedPrivileges aren't listed`, () => { + const features: Feature[] = [ + { + id: 'foo', + name: 'Foo Feature', + icon: 'arrowDown', + app: [], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: ['foo'], + }, + }, + }, + ]; + + const mockXPackMainPlugin = { + getFeatures: jest.fn().mockReturnValue(features), + }; + + const privileges = privilegesFactory(actions, mockXPackMainPlugin as any); + + const actual = privileges.get(); + expect(actual).not.toHaveProperty('reserved.foo'); }); }); diff --git a/x-pack/plugins/security/server/lib/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/lib/authorization/privileges/privileges.ts index 806bc34e838ac..c9e6c39f432f3 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/lib/authorization/privileges/privileges.ts @@ -48,12 +48,14 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl return { features: features.reduce((acc: RawKibanaFeaturePrivileges, feature: Feature) => { - acc[feature.id] = mapValues(feature.privileges, (privilege, privilegeId) => [ - actions.login, - actions.version, - ...featurePrivilegeBuilder.getActions(privilege, feature), - ...(privilegeId === 'all' ? [actions.allHack] : []), - ]); + if (Object.keys(feature.privileges).length > 0) { + acc[feature.id] = mapValues(feature.privileges, (privilege, privilegeId) => [ + actions.login, + actions.version, + ...featurePrivilegeBuilder.getActions(privilege, feature), + ...(privilegeId === 'all' ? [actions.allHack] : []), + ]); + } return acc; }, {}), global: { @@ -71,6 +73,15 @@ export function privilegesFactory(actions: Actions, xpackMainPlugin: XPackMainPl all: [actions.login, actions.version, ...allActions, actions.allHack], read: [actions.login, actions.version, ...readActions], }, + reserved: features.reduce((acc: Record, feature: Feature) => { + if (feature.reserved) { + acc[feature.id] = [ + actions.version, + ...featurePrivilegeBuilder.getActions(feature.reserved!.privilege, feature), + ]; + } + return acc; + }, {}), }; }, }; diff --git a/x-pack/plugins/security/server/lib/authorization/privileges_serializer.test.ts b/x-pack/plugins/security/server/lib/authorization/privileges_serializer.test.ts index 562e9b2c3a475..9c4dbd8e80921 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges_serializer.test.ts +++ b/x-pack/plugins/security/server/lib/authorization/privileges_serializer.test.ts @@ -11,6 +11,7 @@ test(`uses application as top-level key`, () => { global: {}, space: {}, features: {}, + reserved: {}, }); expect(Object.keys(result)).toEqual(['foo-application']); }); @@ -25,6 +26,7 @@ describe('global', () => { }, space: {}, features: {}, + reserved: {}, }); expect(result[application]).toEqual({ all: { @@ -51,6 +53,7 @@ describe('global', () => { }, space: {}, features: {}, + reserved: {}, }); }).toThrowErrorMatchingSnapshot(); }); @@ -66,6 +69,7 @@ describe('space', () => { read: ['action-3', 'action-4'], }, features: {}, + reserved: {}, }); expect(result[application]).toEqual({ space_all: { @@ -92,6 +96,7 @@ describe('space', () => { foo: ['action-1', 'action-2'], }, features: {}, + reserved: {}, }); }).toThrowErrorMatchingSnapshot(); }); @@ -113,6 +118,7 @@ describe('features', () => { qux: ['action-3', 'action-4'], }, }, + reserved: {}, }); expect(result[application]).toEqual({ 'feature_foo.quz': { @@ -154,6 +160,7 @@ describe('features', () => { }, bar: {}, }, + reserved: {}, }); expect(result[application]).toEqual({ 'feature_foo.quz': { @@ -184,6 +191,36 @@ describe('features', () => { bar_baz: ['action-1', 'action-2'], }, }, + reserved: {}, + }); + }); +}); + +describe('reserved', () => { + test(`includes reserved privileges with a reserved_ prefix`, () => { + const application = 'foo-application'; + const result = serializePrivileges(application, { + global: {}, + space: {}, + features: {}, + reserved: { + foo: ['action-1', 'action-2'], + bar: ['action-3', 'action-4'], + }, + }); + expect(result[application]).toEqual({ + reserved_foo: { + application, + name: 'reserved_foo', + actions: ['action-1', 'action-2'], + metadata: {}, + }, + reserved_bar: { + application, + name: 'reserved_bar', + actions: ['action-3', 'action-4'], + metadata: {}, + }, }); }); }); diff --git a/x-pack/plugins/security/server/lib/authorization/privileges_serializer.ts b/x-pack/plugins/security/server/lib/authorization/privileges_serializer.ts index faa40f2e5cf9f..ade90b5c52f90 100644 --- a/x-pack/plugins/security/server/lib/authorization/privileges_serializer.ts +++ b/x-pack/plugins/security/server/lib/authorization/privileges_serializer.ts @@ -73,6 +73,19 @@ export const serializePrivileges = ( }, {} as Record ), + ...Object.entries(privilegeMap.reserved).reduce( + (acc, [privilegeName, privilegeActions]) => { + const name = PrivilegeSerializer.serializeReservedPrivilege(privilegeName); + acc[name] = { + application, + name, + actions: privilegeActions, + metadata: {}, + }; + return acc; + }, + {} as Record + ), }, }; }; diff --git a/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js b/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js index e47c928030638..23a7a0f0d01ab 100644 --- a/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js +++ b/x-pack/plugins/security/server/lib/authorization/register_privileges_with_cluster.test.js @@ -221,6 +221,9 @@ registerPrivilegesWithClusterTest(`inserts privileges when we don't have any exi bar: { read: ['action:bar_read'], } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: null, @@ -250,6 +253,12 @@ registerPrivilegesWithClusterTest(`inserts privileges when we don't have any exi name: 'feature_bar.read', actions: ['action:bar_read'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }); @@ -264,7 +273,8 @@ registerPrivilegesWithClusterTest(`deletes no-longer specified privileges`, { }, space: { read: ['action:bar'] - } + }, + reserved: {}, }, existingPrivileges: { [application]: { @@ -291,6 +301,12 @@ registerPrivilegesWithClusterTest(`deletes no-longer specified privileges`, { name: 'space_baz', actions: ['action:not-baz'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -310,7 +326,7 @@ registerPrivilegesWithClusterTest(`deletes no-longer specified privileges`, { metadata: {}, } } - }, ['read', 'space_baz']); + }, ['read', 'space_baz', 'reserved_customApplication']); } }); @@ -330,6 +346,9 @@ registerPrivilegesWithClusterTest(`updates privileges when global actions don't bar: { read: ['action:quz'] } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: { @@ -355,6 +374,12 @@ registerPrivilegesWithClusterTest(`updates privileges when global actions don't application, name: 'feature_bar.read', actions: ['action:not-quz'], + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -384,6 +409,12 @@ registerPrivilegesWithClusterTest(`updates privileges when global actions don't name: 'feature_bar.read', actions: ['action:quz'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }); @@ -406,6 +437,9 @@ registerPrivilegesWithClusterTest(`updates privileges when space actions don't m bar: { read: ['action:quz'] } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: { @@ -431,6 +465,12 @@ registerPrivilegesWithClusterTest(`updates privileges when space actions don't m application, name: 'feature_bar.read', actions: ['action:quz'], + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -460,6 +500,12 @@ registerPrivilegesWithClusterTest(`updates privileges when space actions don't m name: 'feature_bar.read', actions: ['action:quz'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }); @@ -482,6 +528,9 @@ registerPrivilegesWithClusterTest(`updates privileges when feature actions don't bar: { read: ['action:quz'] } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: { @@ -507,6 +556,12 @@ registerPrivilegesWithClusterTest(`updates privileges when feature actions don't application, name: 'feature_bar.read', actions: ['action:not-quz'], + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -536,15 +591,97 @@ registerPrivilegesWithClusterTest(`updates privileges when feature actions don't name: 'feature_bar.read', actions: ['action:quz'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }); } }); -registerPrivilegesWithClusterTest(`updates privileges when global privilege added`, { +registerPrivilegesWithClusterTest(`updates privileges when reserved actions don't match`, { privilegeMap: { features: {}, + global: { + all: ['action:foo'] + }, + space: { + read: ['action:bar'] + }, + features: { + foo: { + all: ['action:baz'] + } + }, + reserved: { + customApplication: ['action:customApplication'] + } + }, + existingPrivileges: { + [application]: { + all: { + application, + name: 'all', + actions: ['action:foo'], + metadata: {}, + }, + space_read: { + application, + name: 'space_read', + actions: ['action:bar'], + metadata: {}, + }, + 'feature_foo.all': { + application, + name: 'feature_foo.all', + actions: ['action:baz'], + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:not-customApplication'], + metadata: {}, + } + } + }, + assert: ({ expectUpdatedPrivileges }) => { + expectUpdatedPrivileges({ + [application]: { + all: { + application, + name: 'all', + actions: ['action:foo'], + metadata: {}, + }, + space_read: { + application, + name: 'space_read', + actions: ['action:bar'], + metadata: {}, + }, + 'feature_foo.all': { + application, + name: 'feature_foo.all', + actions: ['action:baz'], + metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, + } + } + }); + } +}); + +registerPrivilegesWithClusterTest(`updates privileges when global privilege added`, { + privilegeMap: { global: { all: ['action:foo'], read: ['action:quz'] @@ -556,6 +693,9 @@ registerPrivilegesWithClusterTest(`updates privileges when global privilege adde foo: { all: ['action:foo-all'] } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: { @@ -577,6 +717,12 @@ registerPrivilegesWithClusterTest(`updates privileges when global privilege adde name: 'feature_foo.all', actions: ['action:foo-all'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -606,6 +752,12 @@ registerPrivilegesWithClusterTest(`updates privileges when global privilege adde name: 'feature_foo.all', actions: ['action:foo-all'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }); @@ -614,7 +766,6 @@ registerPrivilegesWithClusterTest(`updates privileges when global privilege adde registerPrivilegesWithClusterTest(`updates privileges when space privilege added`, { privilegeMap: { - features: {}, global: { all: ['action:foo'], }, @@ -626,6 +777,9 @@ registerPrivilegesWithClusterTest(`updates privileges when space privilege added foo: { all: ['action:foo-all'] } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: { @@ -647,6 +801,12 @@ registerPrivilegesWithClusterTest(`updates privileges when space privilege added name: 'feature_foo.all', actions: ['action:foo-all'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -676,6 +836,12 @@ registerPrivilegesWithClusterTest(`updates privileges when space privilege added name: 'feature_foo.all', actions: ['action:foo-all'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }); @@ -696,6 +862,9 @@ registerPrivilegesWithClusterTest(`updates privileges when feature privilege add all: ['action:foo-all'], read: ['action:foo-read'] } + }, + reserved: { + customApplication: ['action:customApplication'] } }, existingPrivileges: { @@ -717,6 +886,12 @@ registerPrivilegesWithClusterTest(`updates privileges when feature privilege add name: 'feature_foo.all', actions: ['action:foo-all'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, } } }, @@ -746,6 +921,97 @@ registerPrivilegesWithClusterTest(`updates privileges when feature privilege add name: 'feature_foo.read', actions: ['action:foo-read'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication'], + metadata: {}, + } + } + }); + } +}); + +registerPrivilegesWithClusterTest(`updates privileges when reserved privilege added`, { + privilegeMap: { + features: {}, + global: { + all: ['action:foo'], + }, + space: { + all: ['action:bar'], + }, + features: { + foo: { + all: ['action:foo-all'], + } + }, + reserved: { + customApplication1: ['action:customApplication1'], + customApplication2: ['action:customApplication2'] + } + }, + existingPrivileges: { + [application]: { + all: { + application, + name: 'foo', + actions: ['action:not-foo'], + metadata: {}, + }, + space_all: { + application, + name: 'space_all', + actions: ['action:not-bar'], + metadata: {}, + }, + 'feature_foo.all': { + application, + name: 'feature_foo.all', + actions: ['action:foo-all'], + metadata: {}, + }, + reserved_customApplication1: { + application, + name: 'reserved_customApplication1', + actions: ['action:customApplication1'], + metadata: {}, + } + } + }, + assert: ({ expectUpdatedPrivileges }) => { + expectUpdatedPrivileges({ + [application]: { + all: { + application, + name: 'all', + actions: ['action:foo'], + metadata: {}, + }, + space_all: { + application, + name: 'space_all', + actions: ['action:bar'], + metadata: {}, + }, + 'feature_foo.all': { + application, + name: 'feature_foo.all', + actions: ['action:foo-all'], + metadata: {}, + }, + reserved_customApplication1: { + application, + name: 'reserved_customApplication1', + actions: ['action:customApplication1'], + metadata: {}, + }, + reserved_customApplication2: { + application, + name: 'reserved_customApplication2', + actions: ['action:customApplication2'], + metadata: {}, } } }); @@ -764,6 +1030,9 @@ registerPrivilegesWithClusterTest(`doesn't update privileges when order of actio foo: { all: ['action:foo-all', 'action:bar-all'] } + }, + reserved: { + customApplication: ['action:customApplication1', 'action:customApplication2'] } }, existingPrivileges: { @@ -785,6 +1054,12 @@ registerPrivilegesWithClusterTest(`doesn't update privileges when order of actio name: 'feature_foo.all', actions: ['action:bar-all', 'action:foo-all'], metadata: {}, + }, + reserved_customApplication: { + application, + name: 'reserved_customApplication', + actions: ['action:customApplication2', 'action:customApplication1'], + metadata: {}, } } }, @@ -797,7 +1072,8 @@ registerPrivilegesWithClusterTest(`throws and logs error when errors getting pri privilegeMap: { features: {}, global: {}, - space: {} + space: {}, + reserved: {}, }, throwErrorWhenGettingPrivileges: new Error('Error getting privileges'), assert: ({ expectErrorThrown }) => { @@ -813,7 +1089,8 @@ registerPrivilegesWithClusterTest(`throws and logs error when errors putting pri }, space: { read: [] - } + }, + reserved: {}, }, existingPrivileges: null, throwErrorWhenPuttingPrivileges: new Error('Error putting privileges'), diff --git a/x-pack/plugins/security/server/lib/authorization/service.ts b/x-pack/plugins/security/server/lib/authorization/service.ts index 4469a832540ec..b465d28c2f6c9 100644 --- a/x-pack/plugins/security/server/lib/authorization/service.ts +++ b/x-pack/plugins/security/server/lib/authorization/service.ts @@ -10,6 +10,7 @@ import { Server } from 'hapi'; import { getClient } from '../../../../../server/lib/get_client_shield'; import { SpacesPlugin } from '../../../../spaces/types'; import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main'; +import { APPLICATION_PREFIX } from '../../../common/constants'; import { OptionalPlugin } from '../optional_plugin'; import { Actions, actionsFactory } from './actions'; import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; @@ -39,7 +40,7 @@ export function createAuthorizationService( const config = server.config(); const actions = actionsFactory(config); - const application = `kibana-${config.get('kibana.index')}`; + const application = `${APPLICATION_PREFIX}${config.get('kibana.index')}`; const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( actions, application, diff --git a/x-pack/plugins/security/server/routes/api/public/privileges/get.test.ts b/x-pack/plugins/security/server/routes/api/public/privileges/get.test.ts index db956eaa6bb0c..e83116365a0fe 100644 --- a/x-pack/plugins/security/server/routes/api/public/privileges/get.test.ts +++ b/x-pack/plugins/security/server/routes/api/public/privileges/get.test.ts @@ -40,6 +40,10 @@ const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => { all: ['*'], read: ['something:/read'], }, + reserved: { + customApplication1: ['custom-action1'], + customApplication2: ['custom-action2'], + }, }; }; @@ -130,6 +134,7 @@ describe('GET privileges', () => { feature1: ['all'], feature2: ['all'], }, + reserved: ['customApplication1', 'customApplication2'], }, }, }); diff --git a/x-pack/plugins/security/server/routes/api/public/privileges/get.ts b/x-pack/plugins/security/server/routes/api/public/privileges/get.ts index c3c1855cbd2e0..273af1b3f0eb9 100644 --- a/x-pack/plugins/security/server/routes/api/public/privileges/get.ts +++ b/x-pack/plugins/security/server/routes/api/public/privileges/get.ts @@ -33,6 +33,7 @@ export function initGetPrivilegesApi( }, {} ), + reserved: Object.keys(privileges.reserved), }; }, config: { diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.js b/x-pack/plugins/security/server/routes/api/public/roles/get.js index 7f3045550ae74..38b50885a76f8 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/get.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/get.js @@ -5,7 +5,7 @@ */ import _ from 'lodash'; import Boom from 'boom'; -import { GLOBAL_RESOURCE } from '../../../../../common/constants'; +import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD } from '../../../../../common/constants'; import { wrapError } from '../../../../lib/errors'; import { PrivilegeSerializer, ResourceSerializer } from '../../../../lib/authorization'; @@ -13,7 +13,59 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, const transformKibanaApplicationsFromEs = (roleApplications) => { const roleKibanaApplications = roleApplications - .filter(roleApplication => roleApplication.application === application); + .filter( + roleApplication => roleApplication.application === application || + roleApplication.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ); + + // if any application entry contains an empty resource, we throw an error + if (roleKibanaApplications.some(entry => entry.resources.length === 0)) { + throw new Error(`ES returned an application entry without resources, can't process this`); + } + + // if there is an entry with the reserved privileges application wildcard + // and there are privileges which aren't reserved, we won't transform these + if (roleKibanaApplications.some(entry => + entry.application === RESERVED_PRIVILEGES_APPLICATION_WILDCARD && + !entry.privileges.every(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) + ) { + return { + success: false + }; + } + + // if space privilege assigned globally, we can't transform these + if (roleKibanaApplications.some(entry => + entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege))) + ) { + return { + success: false + }; + } + + // if global base or reserved privilege assigned at a space, we can't transform these + if (roleKibanaApplications.some(entry => + !entry.resources.includes(GLOBAL_RESOURCE) && + entry.privileges.some(privilege => + PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege) || + PrivilegeSerializer.isSerializedReservedPrivilege(privilege) + )) + ) { + return { + success: false + }; + } + + // if reserved privilege assigned with feature or base privileges, we won't transform these + if (roleKibanaApplications.some(entry => + entry.privileges.some(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)) && + entry.privileges.some(privilege => !PrivilegeSerializer.isSerializedReservedPrivilege(privilege))) + ) { + return { + success: false + }; + } // if any application entry contains the '*' resource in addition to another resource, we can't transform these if (roleKibanaApplications.some(entry => entry.resources.includes(GLOBAL_RESOURCE) && entry.resources.length > 1)) { @@ -42,10 +94,14 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, value: roleKibanaApplications.map(({ resources, privileges }) => { // if we're dealing with a global entry, which we've ensured above is only possible if it's the only item in the array if (resources.length === 1 && resources[0] === GLOBAL_RESOURCE) { + const reservedPrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedReservedPrivilege(privilege)); const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => !PrivilegeSerializer.isSerializedGlobalBasePrivilege(privilege)); + const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); return { + ...reservedPrivileges.length ? { + _reserved: reservedPrivileges.map(privilege => PrivilegeSerializer.deserializeReservedPrivilege(privilege)) + } : {}, base: basePrivileges.map(privilege => PrivilegeSerializer.serializeGlobalBasePrivilege(privilege)), feature: featurePrivileges.reduce((acc, privilege) => { const featurePrivilege = PrivilegeSerializer.deserializeFeaturePrivilege(privilege); @@ -62,7 +118,7 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, } const basePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)); - const featurePrivileges = privileges.filter(privilege => !PrivilegeSerializer.isSerializedSpaceBasePrivilege(privilege)); + const featurePrivileges = privileges.filter(privilege => PrivilegeSerializer.isSerializedFeaturePrivilege(privilege)); return { base: basePrivileges.map(privilege => PrivilegeSerializer.deserializeSpaceBasePrivilege(privilege)), feature: featurePrivileges.reduce((acc, privilege) => { @@ -83,7 +139,10 @@ export function initGetRolesApi(server, callWithRequest, routePreCheckLicenseFn, const transformUnrecognizedApplicationsFromEs = (roleApplications) => { return _.uniq(roleApplications - .filter(roleApplication => roleApplication.application !== application) + .filter(roleApplication => + roleApplication.application !== application && + roleApplication.application !== RESERVED_PRIVILEGES_APPLICATION_WILDCARD + ) .map(roleApplication => roleApplication.application)); }; diff --git a/x-pack/plugins/security/server/routes/api/public/roles/get.test.js b/x-pack/plugins/security/server/routes/api/public/roles/get.test.js index b7b46fc2489c2..1199544de8693 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/get.test.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/get.test.js @@ -8,6 +8,7 @@ import Boom from 'boom'; import { initGetRolesApi } from './get'; const application = 'kibana-.kibana'; +const reservedPrivilegesApplicationWildcard = 'kibana-*'; const createMockServer = () => { const mockServer = new Hapi.Server({ debug: false, port: 8080 }); @@ -243,6 +244,110 @@ describe('GET roles', () => { ], }, }); + + getRolesTest(`transforms matching applications with * resource to kibana _reserved privileges`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'] + } + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + }); + + getRolesTest(`transforms applications with wildcard and * resource to kibana _reserved privileges`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'] + } + ], + _transform_error: [], + _unrecognized_applications: [], + }, + ], + }, + }); }); describe('space', () => { @@ -595,17 +700,22 @@ describe('GET roles', () => { }, }); - getRolesTest(`transforms unrecognized applications`, { + getRolesTest(`space privilege assigned globally returns empty kibana section with _transform_error set to ['kibana']`, { callWithRequestImpl: async () => ({ first_role: { cluster: [], indices: [], applications: [ { - application: 'kibana-.another-kibana', - privileges: ['read'], + application, + privileges: ['space_all'], resources: ['*'], }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + } ], run_as: [], metadata: { @@ -633,24 +743,24 @@ describe('GET roles', () => { run_as: [], }, kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] + _transform_error: ['kibana'], + _unrecognized_applications: [], }, ], }, }); - getRolesTest(`returns a sorted list of roles`, { + getRolesTest(`space privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { callWithRequestImpl: async () => ({ - z_role: { + first_role: { cluster: [], indices: [], applications: [ { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], - }, + application: reservedPrivilegesApplicationWildcard, + privileges: ['space_read'], + resources: ['space:engineering'], + } ], run_as: [], metadata: { @@ -660,33 +770,47 @@ describe('GET roles', () => { enabled: true, }, }, - a_role: { - cluster: [], - indices: [], - applications: [ - { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, }, - ], - run_as: [], - metadata: { - _reserved: true, - }, - transient_metadata: { - enabled: true, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], }, - }, - b_role: { + ], + }, + }); + + getRolesTest(`global base privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { + callWithRequestImpl: async () => ({ + first_role: { cluster: [], indices: [], applications: [ { - application: 'kibana-.another-kibana', - privileges: ['read'], - resources: ['*'], + application, + privileges: ['all'], + resources: ['space:marketing'], }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + } ], run_as: [], metadata: { @@ -701,7 +825,7 @@ describe('GET roles', () => { statusCode: 200, result: [ { - name: 'a_role', + name: 'first_role', metadata: { _reserved: true, }, @@ -714,11 +838,39 @@ describe('GET roles', () => { run_as: [], }, kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + }); + + getRolesTest(`global base privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['all'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, }, + }, + }), + asserts: { + statusCode: 200, + result: [ { - name: 'b_role', + name: 'first_role', metadata: { _reserved: true, }, @@ -731,11 +883,44 @@ describe('GET roles', () => { run_as: [], }, kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + }); + + getRolesTest(`reserved privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, }, + }, + }), + asserts: { + statusCode: 200, + result: [ { - name: 'z_role', + name: 'first_role', metadata: { _reserved: true, }, @@ -748,22 +933,274 @@ describe('GET roles', () => { run_as: [], }, kibana: [], - _transform_error: [], - _unrecognized_applications: ['kibana-.another-kibana'] + _transform_error: ['kibana'], + _unrecognized_applications: [], }, ], }, }); - }); -}); -describe('GET role', () => { - const getRoleTest = ( - description, - { - name, - preCheckLicenseImpl = () => null, - callWithRequestImpl, + getRolesTest( + `reserved privilege assigned with a base privilege returns empty kibana section with _transform_error set to ['kibana']`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'read'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + }); + + getRolesTest( + `reserved privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'feature_foo.foo-privilege-1'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + ], + }, + }); + + getRolesTest(`transforms unrecognized applications`, { + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'] + }, + ], + }, + }); + + getRolesTest(`returns a sorted list of roles`, { + callWithRequestImpl: async () => ({ + z_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + a_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + b_role: { + cluster: [], + indices: [], + applications: [ + { + application: 'kibana-.another-kibana', + privileges: ['read'], + resources: ['*'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: [ + { + name: 'a_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'] + }, + { + name: 'b_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'] + }, + { + name: 'z_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: [], + _unrecognized_applications: ['kibana-.another-kibana'] + }, + ], + }, + }); + }); +}); + +describe('GET role', () => { + const getRoleTest = ( + description, + { + name, + preCheckLicenseImpl = () => null, + callWithRequestImpl, asserts, } ) => { @@ -1018,10 +1455,8 @@ describe('GET role', () => { }, }, }); - }); - describe('space', () => { - getRoleTest(`transforms matching applications with space resources to kibana space base privileges`, { + getRoleTest(`transforms matching applications with * resource to kibana _reserved privileges`, { name: 'first_role', callWithRequestImpl: async () => ({ first_role: { @@ -1030,8 +1465,112 @@ describe('GET role', () => { applications: [ { application, - privileges: ['space_all', 'space_read'], - resources: ['space:marketing', 'space:sales'], + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'] + } + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + }); + + getRoleTest(`transforms applications with wildcard and * resource to kibana _reserved privileges`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['reserved_customApplication1', 'reserved_customApplication2'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [ + { + _reserved: ['customApplication1', 'customApplication2'], + base: [], + feature: {}, + spaces: ['*'] + } + ], + _transform_error: [], + _unrecognized_applications: [], + }, + }, + }); + }); + + describe('space', () => { + getRoleTest(`transforms matching applications with space resources to kibana space base privileges`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all', 'space_read'], + resources: ['space:marketing', 'space:sales'], }, { application, @@ -1284,6 +1823,332 @@ describe('GET role', () => { }, }); + getRoleTest(`space privilege assigned globally returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['space_all'], + resources: ['*'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + + getRoleTest(`space privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['space_read'], + resources: ['space:engineering'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + + getRoleTest(`global base privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['all'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + + getRoleTest(`global base privilege with application wildcard returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application: reservedPrivilegesApplicationWildcard, + privileges: ['all'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + + + getRoleTest(`reserved privilege assigned at a space returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo'], + resources: ['space:marketing'], + }, + { + application, + privileges: ['space_read'], + resources: ['space:engineering'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + + getRoleTest( + `reserved privilege assigned with a base privilege returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'read'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + + getRoleTest( + `reserved privilege assigned with a feature privilege returns empty kibana section with _transform_error set to ['kibana']`, { + name: 'first_role', + callWithRequestImpl: async () => ({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['reserved_foo', 'feature_foo.foo-privilege-1'], + resources: ['*'], + } + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }), + asserts: { + statusCode: 200, + result: { + name: 'first_role', + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + elasticsearch: { + cluster: [], + indices: [], + run_as: [], + }, + kibana: [], + _transform_error: ['kibana'], + _unrecognized_applications: [], + }, + }, + }); + getRoleTest(`transforms unrecognized applications`, { name: 'first_role', callWithRequestImpl: async () => ({ diff --git a/x-pack/plugins/security/server/routes/api/public/roles/put.js b/x-pack/plugins/security/server/routes/api/public/roles/put.js index 5cdf216966fb2..70149da43d338 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/put.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/put.js @@ -32,7 +32,7 @@ export function initPutRolesApi( privilege => PrivilegeSerializer.serializeFeaturePrivilege(featureName, privilege) ) ) - ) : [], + ) : [] ], application, resources: [GLOBAL_RESOURCE] @@ -93,7 +93,7 @@ export function initPutRolesApi( spaces: Joi.alternatives( allSpacesSchema, Joi.array().items(Joi.string().regex(/^[a-z0-9_-]+$/)), - ).default([GLOBAL_RESOURCE]) + ).default([GLOBAL_RESOURCE]), }) ).unique((a, b) => { return intersection(a.spaces, b.spaces).length !== 0; diff --git a/x-pack/plugins/security/server/routes/api/public/roles/put.test.js b/x-pack/plugins/security/server/routes/api/public/roles/put.test.js index 597f9a4b59163..b9d20d5a28e93 100644 --- a/x-pack/plugins/security/server/routes/api/public/roles/put.test.js +++ b/x-pack/plugins/security/server/routes/api/public/roles/put.test.js @@ -45,6 +45,10 @@ const privilegeMap = { 'bar-privilege-1': [], 'bar-privilege-2': [], } + }, + reserved: { + customApplication1: [], + customApplication2: [], } }; @@ -220,6 +224,31 @@ describe('PUT role', () => { }, }); + putRoleTest(`doesn't allow Kibana reserved privileges`, { + name: 'foo-role', + payload: { + kibana: [ + { + _reserved: ['customApplication1'], + spaces: ['*'] + } + ] + }, + asserts: { + statusCode: 400, + result: { + error: 'Bad Request', + //eslint-disable-next-line max-len + message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [\"_reserved\" is not allowed]]`, + statusCode: 400, + validation: { + keys: ['kibana.0._reserved'], + source: 'payload', + }, + }, + }, + }); + putRoleTest(`only allows one global entry`, { name: 'foo-role', payload: { @@ -361,6 +390,31 @@ describe('PUT role', () => { }, }, }); + + putRoleTest(`doesn't allow Kibana reserved privileges`, { + name: 'foo-role', + payload: { + kibana: [ + { + _reserved: ['customApplication1'], + spaces: ['marketing'] + }, + ] + }, + asserts: { + statusCode: 400, + result: { + error: 'Bad Request', + //eslint-disable-next-line max-len + message: `child \"kibana\" fails because [\"kibana\" at position 0 fails because [\"_reserved\" is not allowed]]`, + statusCode: 400, + validation: { + keys: ['kibana.0._reserved'], + source: 'payload' + } + }, + }, + }); }); putRoleTest(`returns result of routePreCheckLicense`, { @@ -410,7 +464,7 @@ describe('PUT role', () => { payload: { kibana: [ { - base: ['all'] + base: ['all'], } ] }, @@ -473,7 +527,7 @@ describe('PUT role', () => { foo: ['foo-privilege-1', 'foo-privilege-2'], bar: ['bar-privilege-1', 'bar-privilege-2'] }, - spaces: ['*'] + spaces: ['*'], }, { base: ['all', 'read'], diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts index b986bcd8d6f56..cfe6719489b9e 100644 --- a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.test.ts @@ -55,6 +55,22 @@ describe('FeatureRegistry', () => { }, }, privilegesTooltip: 'some fancy tooltip', + reserved: { + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + description: 'some completely adequate description', + }, }; const featureRegistry = new FeatureRegistry(); @@ -152,6 +168,32 @@ describe('FeatureRegistry', () => { ); }); + it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: {}, + reserved: { + description: 'something', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` + ); + }); + it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { const feature: Feature = { id: 'test-feature', @@ -178,6 +220,34 @@ describe('FeatureRegistry', () => { ); }); + it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: Feature = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: {}, + reserved: { + description: 'something', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` + ); + }); + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { const feature: Feature = { id: 'test-feature', @@ -210,7 +280,7 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents privileges from specifying management entries that don't exist at the root level`, () => { + it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { const feature: Feature = { id: 'test-feature', name: 'Test Feature', @@ -219,8 +289,10 @@ describe('FeatureRegistry', () => { management: { kibana: ['hey'], }, - privileges: { - all: { + privileges: {}, + reserved: { + description: 'something', + privilege: { catalogue: ['bar'], management: { kibana: ['hey-there'], @@ -238,7 +310,7 @@ describe('FeatureRegistry', () => { const featureRegistry = new FeatureRegistry(); expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown management entries for section kibana: hey-there"` + `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` ); }); diff --git a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts index 95f66157e5781..e536529f7d6d2 100644 --- a/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts +++ b/x-pack/plugins/xpack_main/server/lib/feature_registry/feature_registry.ts @@ -44,6 +44,10 @@ export interface Feature = Privileges catalogue?: string[]; privileges: TPrivileges; privilegesTooltip?: string; + reserved?: { + privilege: FeatureKibanaPrivileges; + description: string; + }; } // Each feature gets its own property on the UICapabilities object, @@ -60,6 +64,25 @@ const managementSchema = Joi.object().pattern( ); const catalogueSchema = Joi.array().items(Joi.string()); +const privilegeSchema = Joi.object({ + grantWithBaseRead: Joi.bool(), + management: managementSchema, + catalogue: catalogueSchema, + api: Joi.array().items(Joi.string()), + app: Joi.array().items(Joi.string()), + savedObject: Joi.object({ + all: Joi.array() + .items(Joi.string()) + .required(), + read: Joi.array() + .items(Joi.string()) + .required(), + }).required(), + ui: Joi.array() + .items(Joi.string().regex(uiCapabilitiesRegex)) + .required(), +}); + const schema = Joi.object({ id: Joi.string() .regex(featurePrivilegePartRegex) @@ -75,30 +98,15 @@ const schema = Joi.object({ .required(), management: managementSchema, catalogue: catalogueSchema, - privileges: Joi.object() - .pattern( - /^(read|all)$/, - Joi.object({ - grantWithBaseRead: Joi.bool(), - management: managementSchema, - catalogue: catalogueSchema, - api: Joi.array().items(Joi.string()), - app: Joi.array().items(Joi.string()), - savedObject: Joi.object({ - all: Joi.array() - .items(Joi.string()) - .required(), - read: Joi.array() - .items(Joi.string()) - .required(), - }).required(), - ui: Joi.array() - .items(Joi.string().regex(uiCapabilitiesRegex)) - .required(), - }) - ) - .required(), + privileges: Joi.object({ + all: privilegeSchema, + read: privilegeSchema, + }).required(), privilegesTooltip: Joi.string(), + reserved: Joi.object({ + privilege: privilegeSchema.required(), + description: Joi.string().required(), + }), }); export class FeatureRegistry { @@ -133,7 +141,12 @@ function validateFeature(feature: FeatureWithAllOrReadPrivileges) { // the following validation can't be enforced by the Joi schema, since it'd require us looking "up" the object graph for the list of valid value, which they explicitly forbid. const { app = [], management = {}, catalogue = [] } = feature; - Object.entries(feature.privileges).forEach(([privilegeId, privilegeDefinition]) => { + const privilegeEntries = [...Object.entries(feature.privileges)]; + if (feature.reserved) { + privilegeEntries.push(['reserved', feature.reserved.privilege]); + } + + privilegeEntries.forEach(([privilegeId, privilegeDefinition]) => { if (!privilegeDefinition) { throw new Error('Privilege definition may not be null or undefined'); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 4d2aefc068ec2..6da0748e607d5 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -467,36 +467,6 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:savedObjectsManagement/graph-workspace/read`, ], }, - monitoring: { - all: [ - 'login:', - `version:${version}`, - `app:${version}:monitoring`, - `app:${version}:kibana`, - `ui:${version}:catalogue/monitoring`, - `ui:${version}:navLinks/monitoring`, - `saved_object:${version}:config/bulk_get`, - `saved_object:${version}:config/get`, - `saved_object:${version}:config/find`, - `ui:${version}:savedObjectsManagement/config/read`, - 'allHack:', - ], - }, - ml: { - all: [ - 'login:', - `version:${version}`, - `app:${version}:ml`, - `app:${version}:kibana`, - `ui:${version}:catalogue/ml`, - `ui:${version}:navLinks/ml`, - `saved_object:${version}:config/bulk_get`, - `saved_object:${version}:config/get`, - `saved_object:${version}:config/find`, - `ui:${version}:savedObjectsManagement/config/read`, - 'allHack:', - ], - }, apm: { all: [ 'login:', @@ -878,12 +848,6 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:savedObjectsManagement/graph-workspace/read`, `ui:${version}:graph/save`, `ui:${version}:graph/delete`, - `app:${version}:monitoring`, - `ui:${version}:catalogue/monitoring`, - `ui:${version}:navLinks/monitoring`, - `app:${version}:ml`, - `ui:${version}:catalogue/ml`, - `ui:${version}:navLinks/ml`, `api:${version}:apm`, `app:${version}:apm`, `ui:${version}:catalogue/apm`, @@ -1004,12 +968,6 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `saved_object:${version}:graph-workspace/get`, `saved_object:${version}:graph-workspace/find`, `ui:${version}:savedObjectsManagement/graph-workspace/read`, - `app:${version}:monitoring`, - `ui:${version}:catalogue/monitoring`, - `ui:${version}:navLinks/monitoring`, - `app:${version}:ml`, - `ui:${version}:catalogue/ml`, - `ui:${version}:navLinks/ml`, `api:${version}:apm`, `app:${version}:apm`, `ui:${version}:catalogue/apm`, @@ -1169,12 +1127,6 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:savedObjectsManagement/graph-workspace/read`, `ui:${version}:graph/save`, `ui:${version}:graph/delete`, - `app:${version}:monitoring`, - `ui:${version}:catalogue/monitoring`, - `ui:${version}:navLinks/monitoring`, - `app:${version}:ml`, - `ui:${version}:catalogue/ml`, - `ui:${version}:navLinks/ml`, `api:${version}:apm`, `app:${version}:apm`, `ui:${version}:catalogue/apm`, @@ -1295,12 +1247,6 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `saved_object:${version}:graph-workspace/get`, `saved_object:${version}:graph-workspace/find`, `ui:${version}:savedObjectsManagement/graph-workspace/read`, - `app:${version}:monitoring`, - `ui:${version}:catalogue/monitoring`, - `ui:${version}:navLinks/monitoring`, - `app:${version}:ml`, - `ui:${version}:catalogue/ml`, - `ui:${version}:navLinks/ml`, `api:${version}:apm`, `app:${version}:apm`, `ui:${version}:catalogue/apm`, @@ -1334,6 +1280,30 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { `ui:${version}:navLinks/uptime`, ], }, + reserved: { + monitoring: [ + `version:${version}`, + `app:${version}:monitoring`, + `app:${version}:kibana`, + `ui:${version}:catalogue/monitoring`, + `ui:${version}:navLinks/monitoring`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + ml: [ + `version:${version}`, + `app:${version}:ml`, + `app:${version}:kibana`, + `ui:${version}:catalogue/ml`, + `ui:${version}:navLinks/ml`, + `saved_object:${version}:config/bulk_get`, + `saved_object:${version}:config/get`, + `saved_object:${version}:config/find`, + `ui:${version}:savedObjectsManagement/config/read`, + ], + }, }); }); @@ -1353,17 +1323,16 @@ export default function({ getService }: KibanaFunctionalTestDefaultProviders) { indexPatterns: ['all', 'read'], timelion: ['all', 'read'], graph: ['all', 'read'], - monitoring: ['all'], - ml: ['all'], - apm: ['all', 'read'], maps: ['all', 'read'], canvas: ['all', 'read'], infrastructure: ['all', 'read'], logs: ['all', 'read'], uptime: ['all', 'read'], + apm: ['all', 'read'], }, global: ['all', 'read'], space: ['all', 'read'], + reserved: ['monitoring', 'ml'], }); }); }); diff --git a/x-pack/test/api_integration/apis/security/roles.js b/x-pack/test/api_integration/apis/security/roles.js index cbe37779f5fb0..5e5c2f3d3b188 100644 --- a/x-pack/test/api_integration/apis/security/roles.js +++ b/x-pack/test/api_integration/apis/security/roles.js @@ -227,6 +227,100 @@ export default function ({ getService }) { }); }); + describe('Get Role', async () => { + it('should get roles', async () => { + await es.shield.putRole({ + name: 'role_to_get', + body: { + cluster: ['manage'], + indices: [ + { + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + allow_restricted_indices: false, + field_security: { + grant: ['*'], + except: ['geo.*'] + }, + query: `{ "match": { "geo.src": "CN" } }`, + }, + ], + applications: [ + { + application: 'kibana-.kibana', + privileges: ['read', 'feature_dashboard.read', 'feature_dev_tools.all'], + resources: ['*'], + }, + { + application: 'kibana-.kibana', + privileges: ['space_all', 'feature_dashboard.read', 'feature_discover.all', 'feature_ml.all'], + resources: ['space:marketing', 'space:sales'], + }, + { + application: 'logstash-default', + privileges: ['logstash-privilege'], + resources: ['*'], + }, + ], + run_as: ['watcher_user'], + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { + enabled: true, + }, + } + }); + + await supertest.get('/api/security/role/role_to_get') + .set('kbn-xsrf', 'xxx') + .expect(200, { + name: 'role_to_get', + metadata: { + foo: 'test-metadata', + }, + transient_metadata: { enabled: true }, + elasticsearch: { + cluster: ['manage'], + indices: [ + { + field_security: { + grant: ['*'], + except: ['geo.*'] + }, + names: ['logstash-*'], + privileges: ['read', 'view_index_metadata'], + query: `{ "match": { "geo.src": "CN" } }`, + allow_restricted_indices: false + }, + ], + run_as: ['watcher_user'], + }, + kibana: [ + { + base: ['read'], + feature: { + dashboard: ['read'], + dev_tools: ['all'], + }, + spaces: ['*'] + }, + { + base: ['all'], + feature: { + dashboard: ['read'], + discover: ['all'], + ml: ['all'] + }, + spaces: ['marketing', 'sales'] + } + ], + + _transform_error: [], + _unrecognized_applications: [ 'logstash-default' ] + }); + }); + }); describe('Delete Role', () => { it('should delete the three roles we created', async () => { await supertest.delete('/api/security/role/empty_role').set('kbn-xsrf', 'xxx').expect(204); diff --git a/x-pack/test/common/services/security/role.ts b/x-pack/test/common/services/security/role.ts index fc7526d22be39..e96f85c702337 100644 --- a/x-pack/test/common/services/security/role.ts +++ b/x-pack/test/common/services/security/role.ts @@ -35,9 +35,11 @@ export class Role { public async delete(name: string) { this.log.debug(`deleting role ${name}`); const { data, status, statusText } = await this.axios.delete(`/api/security/role/${name}`); - if (status !== 204) { + if (status !== 204 && status !== 404) { throw new Error( - `Expected status code of 204, received ${status} ${statusText}: ${util.inspect(data)}` + `Expected status code of 204 or 404, received ${status} ${statusText}: ${util.inspect( + data + )}` ); } this.log.debug(`deleted role ${name}`); diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 7fd316d83d059..0f04a41d88518 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -27,14 +27,14 @@ export default function({ getPageObjects, getService }: KibanaFunctionalTestDefa await esArchiver.loadIfNeeded('logstash_functional'); // ensure we're logged out so we can login as the appropriate users - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); after(async () => { await esArchiver.unload('dashboard/feature_controls/security'); // logout, so the other tests don't accidentally run as the custom users we're testing below - await PageObjects.security.logout(); + await PageObjects.security.forceLogout(); }); describe('global dashboard all privileges', () => { diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts new file mode 100644 index 0000000000000..5bef6cdb76559 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./ml_security')); + loadTestFile(require.resolve('./ml_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts new file mode 100644 index 0000000000000..a6a7a48e8f368 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_security.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.logout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await security.role.delete('global_all_role'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.logout(); + }); + + describe('machine_learning_user', () => { + before(async () => { + await security.user.create('machine_learning_user', { + password: 'machine_learning_user-password', + roles: ['machine_learning_user'], + full_name: 'machine learning user', + }); + }); + + after(async () => { + await security.user.delete('machine_learning_user'); + }); + + it('gets forbidden after login', async () => { + await PageObjects.security.login( + 'machine_learning_user', + 'machine_learning_user-password', + { + expectForbidden: true, + } + ); + }); + }); + + describe('global all', () => { + before(async () => { + await security.user.create('global_all', { + password: 'global_all-password', + roles: ['global_all_role'], + full_name: 'global all', + }); + + await PageObjects.security.login('global_all', 'global_all-password'); + }); + + after(async () => { + await security.user.delete('global_all'); + }); + + it(`doesn't show ml navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Machine Learning'); + }); + }); + + describe('machine_learning_user and global all', () => { + before(async () => { + await security.user.create('machine_learning_user', { + password: 'machine_learning_user-password', + roles: ['machine_learning_user', 'global_all_role'], + full_name: 'machine learning user and global all user', + }); + + await PageObjects.security.login('machine_learning_user', 'machine_learning_user-password'); + }); + + after(async () => { + await security.user.delete('machine_learning_user'); + }); + + it('shows ML navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Machine Learning'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts new file mode 100644 index 0000000000000..36ae88c759e12 --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/feature_controls/ml_spaces.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Machine Learning navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Machine Learning'); + }); + + it(`can navigate to app`, async () => { + await PageObjects.common.navigateToApp('ml', { + basePath: '/s/custom_space', + }); + + await testSubjects.existOrFail('ml-jobs-list'); + }); + }); + + describe('space with ML disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['ml'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show Machine Learning navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Machine Learning'); + }); + + it(`navigating to app returns a 404`, async () => { + await PageObjects.common.navigateToUrl('ml', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/index.ts b/x-pack/test/functional/apps/machine_learning/index.ts new file mode 100644 index 0000000000000..14694cdf3fcbe --- /dev/null +++ b/x-pack/test/functional/apps/machine_learning/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('machine learning', function() { + this.tags('ciGroup3'); + + loadTestFile(require.resolve('./feature_controls')); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/index.ts b/x-pack/test/functional/apps/monitoring/feature_controls/index.ts new file mode 100644 index 0000000000000..35611f391846e --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ loadTestFile }: KibanaFunctionalTestDefaultProviders) { + describe('feature controls', () => { + loadTestFile(require.resolve('./monitoring_security')); + loadTestFile(require.resolve('./monitoring_spaces')); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts new file mode 100644 index 0000000000000..3e6e9cc67efae --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SecurityService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const security: SecurityService = getService('security'); + const appsMenu = getService('appsMenu'); + const PageObjects = getPageObjects(['common', 'security']); + + describe('securty', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + + await security.role.create('global_all_role', { + elasticsearch: { + indices: [{ names: ['logstash-*'], privileges: ['read', 'view_index_metadata'] }], + }, + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], + }); + + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + await security.role.delete('global_all_role'); + + // logout, so the other tests don't accidentally run as the custom users we're testing below + await PageObjects.security.forceLogout(); + }); + + describe('monitoring_user', () => { + before(async () => { + await security.user.create('monitoring_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user'], + full_name: 'monitoring all', + }); + }); + + after(async () => { + await security.user.delete('monitoring_user'); + }); + + it('gets forbidden after login', async () => { + await PageObjects.security.login('monitoring_user', 'monitoring_user-password', { + expectForbidden: true, + }); + }); + }); + + describe('global all', () => { + before(async () => { + await security.user.create('global_all', { + password: 'global_all-password', + roles: ['global_all_role'], + full_name: 'global all', + }); + + await PageObjects.security.login('global_all', 'global_all-password'); + }); + + after(async () => { + await security.user.delete('global_all'); + }); + + it(`doesn't show monitoring navlink`, async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Stack Monitoring'); + }); + }); + + describe('monitoring_user and global all', () => { + before(async () => { + await security.user.create('monitoring_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user', 'global_all_role'], + full_name: 'monitoring user', + }); + + await PageObjects.security.login('monitoring_user', 'monitoring_user-password'); + }); + + after(async () => { + await security.user.delete('monitoring_user'); + }); + + it('shows monitoring navlink', async () => { + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Stack Monitoring'); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts new file mode 100644 index 0000000000000..58282269a5422 --- /dev/null +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_spaces.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { SpacesService } from '../../../../common/services'; +import { KibanaFunctionalTestDefaultProviders } from '../../../../types/providers'; + +// eslint-disable-next-line import/no-default-export +export default function({ getPageObjects, getService }: KibanaFunctionalTestDefaultProviders) { + const esArchiver = getService('esArchiver'); + const spacesService: SpacesService = getService('spaces'); + const PageObjects = getPageObjects(['common', 'dashboard', 'security', 'error']); + const appsMenu = getService('appsMenu'); + const find = getService('find'); + + describe('spaces', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + }); + + describe('space with no features disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: [], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it('shows Stack Monitoring navlink', async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).to.contain('Stack Monitoring'); + }); + + it(`can navigate to app`, async () => { + await PageObjects.common.navigateToApp('monitoring', { + basePath: '/s/custom_space', + }); + + const exists = await find.existsByCssSelector('monitoring-main'); + expect(exists).to.be(true); + }); + }); + + describe('space with monitoring disabled', () => { + before(async () => { + await spacesService.create({ + id: 'custom_space', + name: 'custom_space', + disabledFeatures: ['monitoring'], + }); + }); + + after(async () => { + await spacesService.delete('custom_space'); + }); + + it(`doesn't show Stack Monitoring navlink`, async () => { + await PageObjects.common.navigateToApp('home', { + basePath: '/s/custom_space', + }); + const navLinks = (await appsMenu.readLinks()).map( + (link: Record) => link.text + ); + expect(navLinks).not.to.contain('Stack Monitoring'); + }); + + it(`navigating to app returns a 404`, async () => { + await PageObjects.common.navigateToUrl('monitoring', '', { + basePath: '/s/custom_space', + shouldLoginIfPrompted: false, + ensureCurrentUrl: false, + }); + + await PageObjects.error.expectNotFound(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index a1551f2829a98..a22ad50d1338c 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -8,6 +8,8 @@ export default function ({ loadTestFile }) { describe('Monitoring app', function () { this.tags('ciGroup1'); + loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); loadTestFile(require.resolve('./cluster/alerts')); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 74a79b07f8acc..4f725d04ce034 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -64,15 +64,6 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.login('Rashmi', 'changeme'); }); - //Verify the Access Denied message is displayed - it('Kibana User navigating to Monitoring gets Access Denied', async function () { - const expectedMessage = 'Access Denied'; - await PageObjects.monitoring.navigateTo(); - const actualMessage = await PageObjects.monitoring.getAccessDeniedMessage(); - expect(actualMessage).to.be(expectedMessage); - }); - - it('Kibana User navigating to Management gets permission denied', async function () { await PageObjects.settings.navigateTo(); await PageObjects.security.clickElasticsearchUsers(); diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index ff80559c068bf..1343ad32e8e9b 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -94,6 +94,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/logstash'), resolve(__dirname, './apps/grok_debugger'), resolve(__dirname, './apps/infra'), + resolve(__dirname, './apps/machine_learning'), resolve(__dirname, './apps/rollup_job'), resolve(__dirname, './apps/maps'), resolve(__dirname, './apps/status_page'), @@ -235,6 +236,12 @@ export default async function ({ readConfigFile }) { uptime: { pathname: '/app/uptime', }, + apm: { + pathname: '/app/apm' + }, + ml: { + pathname: '/app/ml' + }, rollupJob: { pathname: '/app/kibana', hash: '/management/elasticsearch/rollup_jobs/' diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js index 4f989965f25fb..fde1eedbfdf53 100644 --- a/x-pack/test/functional/page_objects/security_page.js +++ b/x-pack/test/functional/page_objects/security_page.js @@ -16,7 +16,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const userMenu = getService('userMenu'); - const PageObjects = getPageObjects(['common', 'header', 'settings', 'home']); + const PageObjects = getPageObjects(['common', 'header', 'settings', 'home', 'error']); class LoginPage { async login(username, password, options = {}) { @@ -27,6 +27,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { const expectSpaceSelector = options.expectSpaceSelector || false; const expectSuccess = options.expectSuccess; + const expectForbidden = options.expectForbidden || false; await PageObjects.common.navigateToApp('login'); await testSubjects.setValue('loginUsername', username); @@ -37,6 +38,11 @@ export function SecurityPageProvider({ getService, getPageObjects }) { if (expectSpaceSelector) { await retry.try(() => testSubjects.find('kibanaSpaceSelector')); log.debug(`Finished login process, landed on space selector. currentUrl = ${await browser.getCurrentUrl()}`); + } else if (expectForbidden) { + await retry.try(async () => { + await PageObjects.error.expectForbidden(); + }); + log.debug(`Finished login process, found forbidden message. currentUrl = ${await browser.getCurrentUrl()}`); } else if (expectSuccess) { await find.byCssSelector('[data-test-subj="kibanaChrome"] nav:not(.ng-hide) ', 20000); log.debug(`Finished login process currentUrl = ${await browser.getCurrentUrl()}`); @@ -73,7 +79,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) { async login(username, password, options = {}) { await this.loginPage.login(username, password, options); - if (options.expectSpaceSelector) { + if (options.expectSpaceSelector || options.expectForbidden) { return; } diff --git a/x-pack/test/spaces_api_integration/common/lib/authentication.ts b/x-pack/test/spaces_api_integration/common/lib/authentication.ts index 04ffc291dd602..b901a7ed9516c 100644 --- a/x-pack/test/spaces_api_integration/common/lib/authentication.ts +++ b/x-pack/test/spaces_api_integration/common/lib/authentication.ts @@ -65,4 +65,20 @@ export const AUTHENTICATION = { username: 'a_kibana_rbac_space_1_2_read_user', password: 'password', }, + APM_USER: { + username: 'a_apm_user', + password: 'password', + }, + MACHINE_LEARING_ADMIN: { + username: 'a_machine_learning_admin', + password: 'password', + }, + MACHINE_LEARNING_USER: { + username: 'a_machine_learning_user', + password: 'password', + }, + MONITORING_USER: { + username: 'a_monitoring_user', + password: 'password', + }, }; diff --git a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts index b7b70a7f0a70e..e480548c9493f 100644 --- a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts @@ -320,4 +320,44 @@ export const createUsersAndRoles = async (es: any, supertest: SuperTest) => email: 'a_kibana_rbac_space_1_2_readonly_user@elastic.co', }, }); + + await es.shield.putUser({ + username: AUTHENTICATION.APM_USER.username, + body: { + password: AUTHENTICATION.APM_USER.password, + roles: ['apm_user'], + full_name: 'a apm user', + email: 'a_apm_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MACHINE_LEARING_ADMIN.username, + body: { + password: AUTHENTICATION.MACHINE_LEARING_ADMIN.password, + roles: ['machine_learning_admin'], + full_name: 'a machine learning admin', + email: 'a_machine_learning_admin@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MACHINE_LEARNING_USER.username, + body: { + password: AUTHENTICATION.MACHINE_LEARNING_USER.password, + roles: ['machine_learning_user'], + full_name: 'a machine learning user', + email: 'a_machine_learning_user@elastic.co', + }, + }); + + await es.shield.putUser({ + username: AUTHENTICATION.MONITORING_USER.username, + body: { + password: AUTHENTICATION.MONITORING_USER.password, + roles: ['monitoring_user'], + full_name: 'a monitoring user', + email: 'a_monitoring_user@elastic.co', + }, + }); }; diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts index e1a828b26796f..54557080867cc 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts @@ -35,6 +35,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + apmUser: AUTHENTICATION.APM_USER, + machineLearningAdmin: AUTHENTICATION.MACHINE_LEARING_ADMIN, + machineLearningUser: AUTHENTICATION.MACHINE_LEARNING_USER, + monitoringUser: AUTHENTICATION.MONITORING_USER, }, }, { @@ -51,6 +55,10 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + apmUser: AUTHENTICATION.APM_USER, + machineLearningAdmin: AUTHENTICATION.MACHINE_LEARING_ADMIN, + machineLearningUser: AUTHENTICATION.MACHINE_LEARNING_USER, + monitoringUser: AUTHENTICATION.MONITORING_USER, }, }, ].forEach(scenario => { @@ -180,6 +188,50 @@ export default function getAllSpacesTestSuite({ getService }: TestInvoker) { }, } ); + + getAllTest(`apm_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.apmUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`machine_learning_admin can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.machineLearningAdmin, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`machine_learning_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.machineLearningUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); + + getAllTest(`monitoring_user can't access any spaces from ${scenario.spaceId}`, { + spaceId: scenario.spaceId, + user: scenario.users.monitoringUser, + tests: { + exists: { + statusCode: 403, + response: expectRbacForbidden, + }, + }, + }); }); }); } diff --git a/x-pack/test/ui_capabilities/common/types.ts b/x-pack/test/ui_capabilities/common/types.ts index 38ff44d36e8d1..d7f34b9ecff08 100644 --- a/x-pack/test/ui_capabilities/common/types.ts +++ b/x-pack/test/ui_capabilities/common/types.ts @@ -3,6 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +interface FeaturesPrivileges { + [featureId: string]: string[]; +} // TODO: Consolidate the following type definitions interface CustomRoleSpecificationElasticsearchIndices { @@ -10,28 +13,19 @@ interface CustomRoleSpecificationElasticsearchIndices { privileges: string[]; } -interface CustomRoleSpecification { +export interface RoleKibanaPrivilege { + spaces: string[]; + base?: string[]; + feature?: FeaturesPrivileges; +} + +export interface CustomRoleSpecification { name: string; elasticsearch?: { cluster: string[]; indices: CustomRoleSpecificationElasticsearchIndices[]; }; - kibana?: { - global: { - minimum?: string[]; - feature?: { - [featureName: string]: string[]; - }; - }; - space?: { - [spaceId: string]: { - minimum?: string[]; - feature?: { - [featureName: string]: string[]; - }; - }; - }; - }; + kibana?: RoleKibanaPrivilege[]; } interface ReservedRoleSpecification { @@ -51,7 +45,8 @@ export interface User { username: string; fullName: string; password: string; - role: ReservedRoleSpecification | CustomRoleSpecification; + role?: ReservedRoleSpecification | CustomRoleSpecification; + roles?: Array; } export interface Space { diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index 6c10c43e457b8..6972ede00abe3 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -27,7 +27,14 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau space.id ); switch (scenario.id) { - case 'superuser at everything_space': + case 'superuser at everything_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } case 'global_all at everything_space': case 'dual_privileges_all at everything_space': case 'everything_space_all at everything_space': @@ -36,8 +43,11 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau case 'everything_space_read at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything is enabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts index 29b8d901f23fe..840fbecb18d96 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/index.ts @@ -35,16 +35,20 @@ export default function uiCapabilitiesTests({ } for (const user of Users) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + await securityService.user.create(user.username, { password: user.password, full_name: user.fullName, - roles: [user.role.name], + roles: roles.map(role => role.name), }); - if (isCustomRoleSpecification(user.role)) { - await securityService.role.create(user.role.name, { - elasticsearch: user.role.elasticsearch, - kibana: user.role.kibana, - }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + }); + } } } }); @@ -56,8 +60,12 @@ export default function uiCapabilitiesTests({ for (const user of Users) { await securityService.user.delete(user.username); - if (isCustomRoleSpecification(user.role)) { - await securityService.role.delete(user.role.name); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } } } }); diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts index fc5f1fe66e1c6..7bbf39f11847c 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/nav_links.ts @@ -36,6 +36,10 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul ); switch (scenario.id) { case 'superuser at everything_space': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; case 'global_all at everything_space': case 'dual_privileges_all at everything_space': case 'dual_privileges_read at everything_space': @@ -44,7 +48,9 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul case 'everything_space_read at everything_space': expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); break; case 'superuser at nothing_space': case 'global_all at nothing_space': diff --git a/x-pack/test/ui_capabilities/security_only/scenarios.ts b/x-pack/test/ui_capabilities/security_only/scenarios.ts index eed64379d1ce3..8f7e483417b5f 100644 --- a/x-pack/test/ui_capabilities/security_only/scenarios.ts +++ b/x-pack/test/ui_capabilities/security_only/scenarios.ts @@ -4,12 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { User } from '../common/types'; +import { CustomRoleSpecification, User } from '../common/types'; // For all scenarios, we define both an instance in addition // to a "type" definition so that we can use the exhaustive switch in // typescript to ensure all scenarios are handled. +const allRole: CustomRoleSpecification = { + name: 'all_role', + kibana: [ + { + base: ['all'], + spaces: ['*'], + }, + ], +}; + interface NoKibanaPrivileges extends User { username: 'no_kibana_privileges'; } @@ -121,15 +131,7 @@ const All: All = { username: 'all', fullName: 'all', password: 'all-password', - role: { - name: 'all_role', - kibana: [ - { - base: ['all'], - spaces: ['*'], - }, - ], - }, + role: allRole, }; interface Read extends User { diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index 21e18edef7eab..fe4eca19fe8e8 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -25,15 +25,25 @@ export default function catalogueTests({ getService }: KibanaFunctionalTestDefau password: scenario.password, }); switch (scenario.username) { - case 'superuser': + case 'superuser': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is enabled + const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } case 'all': case 'read': case 'dual_privileges_all': case 'dual_privileges_read': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything is enabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => true); + // everything except ml and monitoring is enabled + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'monitoring' + ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } diff --git a/x-pack/test/ui_capabilities/security_only/tests/index.ts b/x-pack/test/ui_capabilities/security_only/tests/index.ts index 3c0ee0407d404..e69cef8b31360 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/index.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/index.ts @@ -21,15 +21,20 @@ export default function uiCapabilitesTests({ before(async () => { for (const user of UserScenarios) { + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + await securityService.user.create(user.username, { password: user.password, full_name: user.fullName, - roles: [user.role.name], + roles: roles.map(role => role.name), }); - if (isCustomRoleSpecification(user.role)) { - await securityService.role.create(user.role.name, { - kibana: user.role.kibana, - }); + + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.create(role.name, { + kibana: role.kibana, + }); + } } } }); @@ -37,8 +42,12 @@ export default function uiCapabilitesTests({ after(async () => { for (const user of UserScenarios) { await securityService.user.delete(user.username); - if (isCustomRoleSpecification(user.role)) { - await securityService.role.delete(user.role.name); + + const roles = [...(user.role ? [user.role] : []), ...(user.roles ? user.roles : [])]; + for (const role of roles) { + if (isCustomRoleSpecification(role)) { + await securityService.role.delete(role.name); + } } } }); diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index a7777dd324176..4b8cc06bd5688 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -34,13 +34,19 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul }); switch (scenario.username) { case 'superuser': + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('navLinks'); + expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + break; case 'all': case 'read': case 'dual_privileges_all': case 'dual_privileges_read': expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); - expect(uiCapabilities.value!.navLinks).to.eql(navLinksBuilder.all()); + expect(uiCapabilities.value!.navLinks).to.eql( + navLinksBuilder.except('ml', 'monitoring') + ); break; case 'foo_all': case 'foo_read': @@ -50,8 +56,8 @@ export default function navLinksTests({ getService }: KibanaFunctionalTestDefaul navLinksBuilder.only('management', 'foo') ); break; - case 'no_kibana_privileges': case 'legacy_all': + case 'no_kibana_privileges': expect(uiCapabilities.success).to.be(false); expect(uiCapabilities.failureReason).to.be(GetUICapabilitiesFailureReason.NotFound); break;