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;