diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx
index 0fd03481eec1d..9d2457b38cfd8 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx
@@ -23,6 +23,7 @@ import { get } from 'lodash';
import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react';
import { toastNotifications } from 'ui/notify';
import { Space } from '../../../../../../spaces/common/model/space';
+import { UserProfile } from '../../../../../../xpack_main/public/services/user_profile';
import { IndexPrivilege } from '../../../../../common/model/index_privilege';
import { KibanaPrivilege } from '../../../../../common/model/kibana_privilege';
import { Role } from '../../../../../common/model/role';
@@ -46,6 +47,7 @@ interface Props {
notifier: any;
spaces?: Space[];
spacesEnabled: boolean;
+ userProfile: UserProfile;
}
interface State {
@@ -212,6 +214,7 @@ export class EditRolePage extends Component {
kibanaAppPrivileges={this.props.kibanaAppPrivileges}
spaces={this.props.spaces}
spacesEnabled={this.props.spacesEnabled}
+ userProfile={this.props.userProfile}
editable={!isReservedRole(this.state.role)}
role={this.state.role}
onChange={this.onRoleChange}
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap
index 85f0acaf92a40..3a8d878dd9419 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges.test.tsx.snap
@@ -42,6 +42,11 @@ exports[` renders without crashing 1`] = `
},
]
}
+ userProfile={
+ Object {
+ "hasCapability": [Function],
+ }
+ }
validator={
RoleValidator {
"inProgressSpacePrivileges": Array [],
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap
index 07e626b541350..eb59b66483292 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/space_aware_privilege_form.test.tsx.snap
@@ -217,9 +217,39 @@ exports[` renders without crashing 1`] = `
},
]
}
+ userProfile={
+ Object {
+ "hasCapability": [Function],
+ }
+ }
/>
`;
+
+exports[` with user profile disabling "manageSpaces" renders a warning message instead of the privilege form 1`] = `
+
+ Insufficient Privileges
+
+ }
+>
+
+ You are not authorized to view all available spaces.
+
+
+ Please ensure your account has all privileges granted by the
+
+
+ kibana_user
+
+ role, and try again.
+
+
+`;
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx
index 674b44ef3dbf6..aafe9d273c5c4 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.test.tsx
@@ -35,6 +35,9 @@ const buildProps = (customProps = {}) => {
name: 'Marketing',
},
],
+ userProfile: {
+ hasCapability: () => true,
+ },
kibanaAppPrivileges: [
{
name: 'all',
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx
index 63c595314e24d..e8d53770270db 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/impacted_spaces_flyout.tsx
@@ -17,6 +17,7 @@ import { PrivilegeSpaceTable } from './privilege_space_table';
import { Space } from '../../../../../../../../spaces/common/model/space';
import { ManageSpacesButton } from '../../../../../../../../spaces/public/components';
+import { UserProfile } from '../../../../../../../../xpack_main/public/services/user_profile';
import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege';
import { Role } from '../../../../../../../common/model/role';
import { NO_PRIVILEGE_VALUE } from '../../../lib/constants';
@@ -25,6 +26,7 @@ import './impacted_spaces_flyout.less';
interface Props {
role: Role;
spaces: Space[];
+ userProfile: UserProfile;
}
interface State {
@@ -118,7 +120,7 @@ export class ImpactedSpacesFlyout extends Component {
{/* TODO: Hide footer if button is not available */}
-
+
);
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx
index 82a1133f7aaa2..07dd40c5de8a5 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.test.tsx
@@ -38,6 +38,7 @@ const buildProps = (customProps = {}) => {
name: 'Marketing',
},
],
+ userProfile: { hasCapability: () => true },
editable: true,
kibanaAppPrivileges: ['all' as KibanaPrivilege],
onChange: jest.fn(),
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx
index 1ff7b62f715a7..80d848b5893d5 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/kibana_privileges.tsx
@@ -6,6 +6,7 @@
import React, { Component } from 'react';
import { Space } from '../../../../../../../../spaces/common/model/space';
+import { UserProfile } from '../../../../../../../../xpack_main/public/services/user_profile';
import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege';
import { Role } from '../../../../../../../common/model/role';
import { RoleValidator } from '../../../lib/validate_role';
@@ -17,6 +18,7 @@ interface Props {
role: Role;
spacesEnabled: boolean;
spaces?: Space[];
+ userProfile: UserProfile;
editable: boolean;
kibanaAppPrivileges: KibanaPrivilege[];
onChange: (role: Role) => void;
@@ -38,6 +40,7 @@ export class KibanaPrivileges extends Component {
role,
spacesEnabled,
spaces = [],
+ userProfile,
onChange,
editable,
validator,
@@ -49,6 +52,7 @@ export class KibanaPrivileges extends Component {
kibanaAppPrivileges={kibanaAppPrivileges}
role={role}
spaces={spaces}
+ userProfile={userProfile}
onChange={onChange}
editable={editable}
validator={validator}
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.test.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.test.tsx
index bcc5963743610..b386fcafc614c 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.test.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.test.tsx
@@ -37,6 +37,7 @@ const buildProps = (customProps: any = {}) => {
name: 'Marketing',
},
],
+ userProfile: { hasCapability: () => true },
editable: true,
kibanaAppPrivileges: ['all', 'read'],
onChange: jest.fn(),
@@ -229,4 +230,22 @@ describe('', () => {
expect(addPrivilegeButton).toHaveLength(1);
});
});
+
+ describe('with user profile disabling "manageSpaces"', () => {
+ it('renders a warning message instead of the privilege form', () => {
+ const props = buildProps({
+ userProfile: {
+ hasCapability: (capability: string) => {
+ if (capability === 'manageSpaces') {
+ return false;
+ }
+ throw new Error(`unexpected call to hasCapability: ${capability}`);
+ },
+ },
+ });
+
+ const wrapper = shallow();
+ expect(wrapper).toMatchSnapshot();
+ });
+ });
});
diff --git a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx
index 2801862cabf94..dcdaf055cae8d 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx
+++ b/x-pack/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_form.tsx
@@ -6,6 +6,7 @@
import {
EuiButton,
+ EuiCallOut,
// @ts-ignore
EuiDescribedFormGroup,
EuiFlexGroup,
@@ -17,6 +18,7 @@ import {
} from '@elastic/eui';
import React, { Component, Fragment } from 'react';
import { Space } from '../../../../../../../../spaces/common/model/space';
+import { UserProfile } from '../../../../../../../../xpack_main/public/services/user_profile';
import { KibanaPrivilege } from '../../../../../../../common/model/kibana_privilege';
import { Role } from '../../../../../../../common/model/role';
import { isReservedRole } from '../../../../../../lib/role';
@@ -37,6 +39,7 @@ interface Props {
onChange: (role: Role) => void;
editable: boolean;
validator: RoleValidator;
+ userProfile: UserProfile;
}
interface PrivilegeForm {
@@ -70,7 +73,19 @@ export class SpaceAwarePrivilegeForm extends Component {
}
public render() {
- const { kibanaAppPrivileges, role } = this.props;
+ const { kibanaAppPrivileges, role, userProfile } = this.props;
+
+ if (!userProfile.hasCapability('manageSpaces')) {
+ return (
+ Insufficient Privileges} iconType="alert" color="danger">
+ You are not authorized to view all available spaces.
+
+ Please ensure your account has all privileges granted by the{' '}
+ kibana_user role, and try again.
+
+
+ );
+ }
const assignedPrivileges = role.kibana;
@@ -190,7 +205,11 @@ export class SpaceAwarePrivilegeForm extends Component {
)}
-
+
diff --git a/x-pack/plugins/security/public/views/management/edit_role/index.js b/x-pack/plugins/security/public/views/management/edit_role/index.js
index 5c744c908824c..0d71749d0f121 100644
--- a/x-pack/plugins/security/public/views/management/edit_role/index.js
+++ b/x-pack/plugins/security/public/views/management/edit_role/index.js
@@ -19,6 +19,7 @@ import 'plugins/security/services/shield_indices';
import { IndexPatternsProvider } from 'ui/index_patterns/index_patterns';
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
+import { UserProfileProvider } from 'plugins/xpack_main/services/user_profile';
import { SpacesManager } from 'plugins/spaces/lib';
import { checkLicenseError } from 'plugins/security/lib/check_license_error';
import { EDIT_ROLES_PATH, ROLES_PATH } from '../management_urls';
@@ -92,6 +93,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
const role = $route.current.locals.role;
const xpackInfo = Private(XPackInfoProvider);
+ const userProfile = Private(UserProfileProvider);
const allowDocumentLevelSecurity = xpackInfo.get('features.security.allowRoleDocumentLevelSecurity');
const allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity');
const rbacApplication = chrome.getInjected('rbacApplication');
@@ -137,6 +139,7 @@ routes.when(`${EDIT_ROLES_PATH}/:name?`, {
notifier={Notifier}
spaces={spaces}
spacesEnabled={enableSpaceAwarePrivileges}
+ userProfile={userProfile}
/>, domNode);
// unmount react on controller destroy
diff --git a/x-pack/plugins/spaces/index.js b/x-pack/plugins/spaces/index.js
index b7cfeb94f958e..7164854a11e2b 100644
--- a/x-pack/plugins/spaces/index.js
+++ b/x-pack/plugins/spaces/index.js
@@ -19,6 +19,7 @@ import { wrapError } from './server/lib/errors';
import mappings from './mappings.json';
import { spacesSavedObjectsClientWrapperFactory } from './server/lib/saved_objects_client/saved_objects_client_wrapper_factory';
import { watchStatusAndLicenseToInitialize } from '../../server/lib/watch_status_and_license_to_initialize';
+import { registerUserProfileCapabilityFactory } from '../xpack_main/server/lib/user_profile_registry';
import { SpacesClient } from './server/lib/spaces_client';
import { SpacesAuditLogger } from './server/lib/audit_logger';
import { AuditLogger } from '../../server/lib/audit_logger';
@@ -124,6 +125,14 @@ export const spaces = (kibana) => new kibana.Plugin({
initSpacesRequestInterceptors(server);
+ registerUserProfileCapabilityFactory(async (request) => {
+ const spacesClient = server.plugins.spaces.spacesClient.getScopedClient(request);
+
+ return {
+ manageSpaces: await spacesClient.canEnumerateSpaces(),
+ };
+ });
+
// Register a function with server to manage the collection of usage stats
server.usage.collectorSet.register(getSpacesUsageCollector(server));
}
diff --git a/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx b/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx
index 81113fd4cde87..eb70629255ed0 100644
--- a/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx
+++ b/x-pack/plugins/spaces/public/components/manage_spaces_button.tsx
@@ -6,16 +6,22 @@
import { EuiButton } from '@elastic/eui';
import React, { Component, CSSProperties } from 'react';
+import { UserProfile } from '../../../xpack_main/public/services/user_profile';
import { MANAGE_SPACES_URL } from '../lib/constants';
interface Props {
isDisabled?: boolean;
size?: 's' | 'l';
style?: CSSProperties;
+ userProfile: UserProfile;
}
export class ManageSpacesButton extends Component {
public render() {
+ if (!this.props.userProfile.hasCapability('manageSpaces')) {
+ return null;
+ }
+
return (
(
+ Permission denied}
+ body={
+ You do not have permission to manage spaces.
+ }
+ />
+);
diff --git a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js
index 7524fa11dfd8d..0ff739e127656 100644
--- a/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js
+++ b/x-pack/plugins/spaces/public/views/management/edit_space/manage_space_page.js
@@ -34,6 +34,7 @@ import { CustomizeSpaceAvatar } from './customize_space_avatar';
import { isReservedSpace } from '../../../../common';
import { ReservedSpaceBadge } from './reserved_space_badge';
import { SpaceValidator } from '../lib/validate_space';
+import { UnauthorizedPrompt } from '../components/unauthorized_prompt';
export class ManageSpacePage extends Component {
state = {
@@ -99,6 +100,14 @@ export class ManageSpacePage extends Component {
}
getForm = () => {
+ const {
+ userProfile
+ } = this.props;
+
+ if (!userProfile.hasCapability('manageSpaces')) {
+ return ;
+ }
+
const {
name = '',
description = '',
@@ -336,4 +345,5 @@ ManageSpacePage.propTypes = {
space: PropTypes.string,
spacesManager: PropTypes.object,
spacesNavState: PropTypes.object.isRequired,
+ userProfile: PropTypes.object.isRequired,
};
diff --git a/x-pack/plugins/spaces/public/views/management/page_routes.js b/x-pack/plugins/spaces/public/views/management/page_routes.js
index 07f97439184fe..17bb29ac6282a 100644
--- a/x-pack/plugins/spaces/public/views/management/page_routes.js
+++ b/x-pack/plugins/spaces/public/views/management/page_routes.js
@@ -7,6 +7,7 @@
import 'ui/autoload/styles';
import 'plugins/spaces/views/management/manage_spaces.less';
import template from 'plugins/spaces/views/management/template.html';
+import { UserProfileProvider } from 'plugins/xpack_main/services/user_profile';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
@@ -20,7 +21,9 @@ const reactRootNodeId = 'manageSpacesReactRoot';
routes.when('/management/spaces/list', {
template,
- controller: function ($scope, $http, chrome, spacesNavState, spaceSelectorURL) {
+ controller: function ($scope, $http, chrome, Private, spacesNavState, spaceSelectorURL) {
+ const userProfile = Private(UserProfileProvider);
+
$scope.$$postDigest(() => {
const domNode = document.getElementById(reactRootNodeId);
@@ -29,6 +32,7 @@ routes.when('/management/spaces/list', {
render(, domNode);
// unmount react on controller destroy
@@ -41,7 +45,9 @@ routes.when('/management/spaces/list', {
routes.when('/management/spaces/create', {
template,
- controller: function ($scope, $http, chrome, spacesNavState, spaceSelectorURL) {
+ controller: function ($scope, $http, chrome, Private, spacesNavState, spaceSelectorURL) {
+ const userProfile = Private(UserProfileProvider);
+
$scope.$$postDigest(() => {
const domNode = document.getElementById(reactRootNodeId);
@@ -50,6 +56,7 @@ routes.when('/management/spaces/create', {
render(, domNode);
// unmount react on controller destroy
@@ -66,7 +73,9 @@ routes.when('/management/spaces/edit', {
routes.when('/management/spaces/edit/:space', {
template,
- controller: function ($scope, $http, $route, chrome, spacesNavState, spaceSelectorURL) {
+ controller: function ($scope, $http, $route, chrome, Private, spacesNavState, spaceSelectorURL) {
+ const userProfile = Private(UserProfileProvider);
+
$scope.$$postDigest(() => {
const domNode = document.getElementById(reactRootNodeId);
@@ -81,6 +90,7 @@ routes.when('/management/spaces/edit/:space', {
chrome={chrome}
spacesManager={spacesManager}
spacesNavState={spacesNavState}
+ userProfile={userProfile}
/>, domNode);
// unmount react on controller destroy
diff --git a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx
index 83a7d4149a25d..f627159718452 100644
--- a/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx
+++ b/x-pack/plugins/spaces/public/views/management/spaces_grid/spaces_grid_page.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { Component } from 'react';
+import React, { Component, Fragment } from 'react';
import {
EuiButton,
@@ -22,15 +22,18 @@ import {
import { toastNotifications } from 'ui/notify';
+import { UserProfile } from '../../../../../xpack_main/public/services/user_profile';
import { isReservedSpace } from '../../../../common';
import { Space } from '../../../../common/model/space';
import { SpaceAvatar } from '../../../components';
import { SpacesManager } from '../../../lib/spaces_manager';
import { ConfirmDeleteModal } from '../components/confirm_delete_modal';
+import { UnauthorizedPrompt } from '../components/unauthorized_prompt';
interface Props {
spacesManager: SpacesManager;
spacesNavState: any;
+ userProfile: UserProfile;
}
interface State {
@@ -61,38 +64,48 @@ export class SpacesGridPage extends Component {
return (
-
-
-
-
- Spaces
-
-
- {this.getPrimaryActionButton()}
-
-
-
-
-
+ {this.getPageContent()}
{this.getConfirmDeleteModal()}
);
}
+ public getPageContent() {
+ if (!this.props.userProfile.hasCapability('manageSpaces')) {
+ return ;
+ }
+
+ return (
+
+
+
+
+ Spaces
+
+
+ {this.getPrimaryActionButton()}
+
+
+
+
+
+ );
+ }
+
public getPrimaryActionButton() {
return (
-
+
`;
diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap b/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap
index b46ae0481840a..ae120ec8fbab2 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap
+++ b/x-pack/plugins/spaces/public/views/nav_control/components/__snapshots__/spaces_description.test.tsx.snap
@@ -26,6 +26,11 @@ exports[`SpacesDescription renders without crashing 1`] = `
"width": "100%",
}
}
+ userProfile={
+ Object {
+ "hasCapability": [Function],
+ }
+ }
/>
diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx
index 0308b8b5a4d21..9d8dd36999c75 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx
+++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.test.tsx
@@ -10,6 +10,8 @@ import { SpacesDescription } from './spaces_description';
describe('SpacesDescription', () => {
it('renders without crashing', () => {
- expect(shallow()).toMatchSnapshot();
+ expect(
+ shallow( true }} />)
+ ).toMatchSnapshot();
});
});
diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx
index 127595c930ee4..8d033996f6a8d 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx
+++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_description.tsx
@@ -6,11 +6,16 @@
import { EuiContextMenuPanel, EuiText } from '@elastic/eui';
import React, { SFC } from 'react';
+import { UserProfile } from '../../../../../xpack_main/public/services/user_profile';
import { ManageSpacesButton } from '../../../components';
import { SPACES_FEATURE_DESCRIPTION } from '../../../lib/constants';
import './spaces_description.less';
-export const SpacesDescription: SFC = () => {
+interface Props {
+ userProfile: UserProfile;
+}
+
+export const SpacesDescription: SFC = (props: Props) => {
const panelProps = {
className: 'spacesDescription',
title: 'Spaces',
@@ -22,7 +27,7 @@ export const SpacesDescription: SFC = () => {
{SPACES_FEATURE_DESCRIPTION}
-
+
);
diff --git a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx
index 2376914ec1b11..7827e26555230 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx
+++ b/x-pack/plugins/spaces/public/views/nav_control/components/spaces_menu.tsx
@@ -6,6 +6,7 @@
import { EuiContextMenuItem, EuiContextMenuPanel, EuiFieldSearch, EuiText } from '@elastic/eui';
import React, { Component } from 'react';
+import { UserProfile } from '../../../../../xpack_main/public/services/user_profile';
import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../common/constants';
import { Space } from '../../../../common/model/space';
import { ManageSpacesButton, SpaceAvatar } from '../../../components';
@@ -14,6 +15,7 @@ import './spaces_menu.less';
interface Props {
spaces: Space[];
onSelectSpace: (space: Space) => void;
+ userProfile: UserProfile;
}
interface State {
@@ -133,7 +135,11 @@ export class SpacesMenu extends Component {
private renderManageButton = () => {
return (
-
+
);
};
diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control.js b/x-pack/plugins/spaces/public/views/nav_control/nav_control.js
index c1599ed1a6bc5..657194a09a5a8 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/nav_control.js
+++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control.js
@@ -10,6 +10,7 @@ import { uiModules } from 'ui/modules';
import { SpacesManager } from 'plugins/spaces/lib/spaces_manager';
import template from 'plugins/spaces/views/nav_control/nav_control.html';
import 'plugins/spaces/views/nav_control/nav_control.less';
+import { UserProfileProvider } from 'plugins/xpack_main/services/user_profile';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
@@ -25,7 +26,9 @@ const module = uiModules.get('spaces_nav', ['kibana']);
let spacesManager;
-module.controller('spacesNavController', ($scope, $http, chrome, activeSpace, spaceSelectorURL) => {
+module.controller('spacesNavController', ($scope, $http, chrome, Private, activeSpace, spaceSelectorURL) => {
+ const userProfile = Private(UserProfileProvider);
+
const domNode = document.getElementById(`spacesNavReactRoot`);
spacesManager = new SpacesManager($http, chrome, spaceSelectorURL);
@@ -34,7 +37,7 @@ module.controller('spacesNavController', ($scope, $http, chrome, activeSpace, sp
$scope.$parent.$watch('isVisible', function (isVisible) {
if (isVisible && !mounted) {
- render(, domNode);
+ render(, domNode);
mounted = true;
}
});
diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.js b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.js
index 0c35f175d930d..9252055e33957 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.js
+++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.test.js
@@ -41,7 +41,11 @@ describe('NavControlPopover', () => {
const spacesManager = new SpacesManager(createMockHttpAgent(), mockChrome);
- const wrapper = shallow();
+ const wrapper = shallow( true }}
+ />);
expect(wrapper).toMatchSnapshot();
});
@@ -55,7 +59,11 @@ describe('NavControlPopover', () => {
const spacesManager = new SpacesManager(mockAgent, mockChrome);
- const wrapper = mount();
+ const wrapper = mount( true }}
+ />);
return new Promise((resolve) => {
setTimeout(() => {
diff --git a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx
index 6bd643de0f3f1..bdf45b7c61bac 100644
--- a/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx
+++ b/x-pack/plugins/spaces/public/views/nav_control/nav_control_popover.tsx
@@ -6,6 +6,7 @@
import { EuiAvatar, EuiPopover } from '@elastic/eui';
import React, { Component } from 'react';
+import { UserProfile } from '../../../../xpack_main/public/services/user_profile';
import { Space } from '../../../common/model/space';
import { SpaceAvatar } from '../../components';
import { SpacesManager } from '../../lib/spaces_manager';
@@ -19,6 +20,7 @@ interface Props {
error: string;
space: Space;
};
+ userProfile: UserProfile;
}
interface State {
@@ -57,9 +59,15 @@ export class NavControlPopover extends Component {
let element: React.ReactNode;
if (this.state.spaces.length < 2) {
- element = ;
+ element = ;
} else {
- element = ;
+ element = (
+
+ );
}
return (
diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts
index 6aaed0bb9506c..e272d7d745498 100644
--- a/x-pack/plugins/spaces/server/lib/spaces_client.test.ts
+++ b/x-pack/plugins/spaces/server/lib/spaces_client.test.ts
@@ -241,6 +241,109 @@ describe('#getAll', () => {
});
});
+describe('#canEnumerateSpaces', () => {
+ describe(`authorization is null`, () => {
+ test(`returns true`, async () => {
+ const mockAuditLogger = createMockAuditLogger();
+ const authorization = null;
+ const request = Symbol();
+
+ const client = new SpacesClient(mockAuditLogger as any, authorization, null, null, request);
+
+ const canEnumerateSpaces = await client.canEnumerateSpaces();
+ expect(canEnumerateSpaces).toEqual(true);
+ expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
+ expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe(`authorization.mode.useRbacForRequest is false`, () => {
+ test(`returns true`, async () => {
+ const mockAuditLogger = createMockAuditLogger();
+ const { mockAuthorization } = createMockAuthorization();
+ mockAuthorization.mode.useRbacForRequest.mockReturnValue(false);
+ const request = Symbol();
+
+ const client = new SpacesClient(
+ mockAuditLogger as any,
+ mockAuthorization,
+ null,
+ null,
+ request
+ );
+ const canEnumerateSpaces = await client.canEnumerateSpaces();
+
+ expect(canEnumerateSpaces).toEqual(true);
+ expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
+ expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('useRbacForRequest is true', () => {
+ test(`returns false if user is not authorized to enumerate spaces`, async () => {
+ const username = Symbol();
+ const mockAuditLogger = createMockAuditLogger();
+ const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization();
+ mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
+ mockCheckPrivilegesGlobally.mockReturnValue({
+ username,
+ hasAllRequested: false,
+ });
+ const request = Symbol();
+
+ const client = new SpacesClient(
+ mockAuditLogger as any,
+ mockAuthorization,
+ null,
+ null,
+ request
+ );
+
+ const canEnumerateSpaces = await client.canEnumerateSpaces();
+ expect(canEnumerateSpaces).toEqual(false);
+
+ expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
+ expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith(
+ mockAuthorization.actions.manageSpaces
+ );
+
+ expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
+ expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
+ });
+
+ test(`returns true if user is authorized to enumerate spaces`, async () => {
+ const username = Symbol();
+ const mockAuditLogger = createMockAuditLogger();
+ const { mockAuthorization, mockCheckPrivilegesGlobally } = createMockAuthorization();
+ mockAuthorization.mode.useRbacForRequest.mockReturnValue(true);
+ mockCheckPrivilegesGlobally.mockReturnValue({
+ username,
+ hasAllRequested: true,
+ });
+ const request = Symbol();
+
+ const client = new SpacesClient(
+ mockAuditLogger as any,
+ mockAuthorization,
+ null,
+ null,
+ request
+ );
+
+ const canEnumerateSpaces = await client.canEnumerateSpaces();
+ expect(canEnumerateSpaces).toEqual(true);
+
+ expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request);
+ expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith(
+ mockAuthorization.actions.manageSpaces
+ );
+
+ expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0);
+ expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0);
+ });
+ });
+});
+
describe('#get', () => {
const savedObject = {
id: 'foo',
diff --git a/x-pack/plugins/spaces/server/lib/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client.ts
index 2942b5a019fcb..96bfd995ce56b 100644
--- a/x-pack/plugins/spaces/server/lib/spaces_client.ts
+++ b/x-pack/plugins/spaces/server/lib/spaces_client.ts
@@ -30,6 +30,19 @@ export class SpacesClient {
this.request = request;
}
+ public async canEnumerateSpaces(): Promise {
+ if (this.useRbac()) {
+ const checkPrivileges = this.authorization.checkPrivilegesWithRequest(this.request);
+ const { hasAllRequested } = await checkPrivileges.globally(
+ this.authorization.actions.manageSpaces
+ );
+ return hasAllRequested;
+ }
+
+ // If not RBAC, then we are legacy, and all legacy users can enumerate all spaces
+ return true;
+ }
+
public async getAll(): Promise<[Space]> {
if (this.useRbac()) {
const { saved_objects } = await this.internalSavedObjectRepository.find({
diff --git a/x-pack/plugins/xpack_main/index.js b/x-pack/plugins/xpack_main/index.js
index 6e6ed25893ced..231bed0fe0348 100644
--- a/x-pack/plugins/xpack_main/index.js
+++ b/x-pack/plugins/xpack_main/index.js
@@ -88,6 +88,7 @@ export const xpackMain = (kibana) => {
telemetryUrl: config.get('xpack.xpack_main.telemetry.url'),
telemetryEnabled: isTelemetryEnabled(config),
telemetryOptedIn: null,
+ userProfile: {},
};
},
hacks: [
diff --git a/x-pack/plugins/xpack_main/public/services/user_profile.test.ts b/x-pack/plugins/xpack_main/public/services/user_profile.test.ts
new file mode 100644
index 0000000000000..45507ab604284
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/services/user_profile.test.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { UserProfileProvider } from './user_profile';
+
+describe('UserProfile', () => {
+ it('should return true when the specified capability is enabled', () => {
+ const capabilities = {
+ test1: true,
+ test2: false,
+ };
+
+ const userProfile = UserProfileProvider(capabilities);
+
+ expect(userProfile.hasCapability('test1')).toEqual(true);
+ });
+
+ it('should return false when the specified capability is disabled', () => {
+ const capabilities = {
+ test1: true,
+ test2: false,
+ };
+
+ const userProfile = UserProfileProvider(capabilities);
+
+ expect(userProfile.hasCapability('test2')).toEqual(false);
+ });
+
+ it('should return the default value when the specified capability is not defined', () => {
+ const capabilities = {
+ test1: true,
+ test2: false,
+ };
+
+ const userProfile = UserProfileProvider(capabilities);
+
+ expect(userProfile.hasCapability('test3')).toEqual(true);
+ expect(userProfile.hasCapability('test3', false)).toEqual(false);
+ });
+});
diff --git a/x-pack/plugins/xpack_main/public/services/user_profile.ts b/x-pack/plugins/xpack_main/public/services/user_profile.ts
new file mode 100644
index 0000000000000..09b257aa80e3f
--- /dev/null
+++ b/x-pack/plugins/xpack_main/public/services/user_profile.ts
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+interface Capabilities {
+ [capability: string]: boolean;
+}
+
+export interface UserProfile {
+ hasCapability: (capability: string) => boolean;
+}
+
+export function UserProfileProvider(userProfile: Capabilities) {
+ class UserProfileClass implements UserProfile {
+ private capabilities: Capabilities;
+
+ constructor(profileData: Capabilities = {}) {
+ this.capabilities = {
+ ...profileData,
+ };
+ }
+
+ public hasCapability(capability: string, defaultValue: boolean = true): boolean {
+ return capability in this.capabilities ? this.capabilities[capability] : defaultValue;
+ }
+ }
+
+ return new UserProfileClass(userProfile);
+}
diff --git a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js
index 990e4e1a7d53a..b8362c6549e16 100644
--- a/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js
+++ b/x-pack/plugins/xpack_main/server/lib/replace_injected_vars.js
@@ -5,13 +5,15 @@
*/
import { getTelemetryOptIn } from "./get_telemetry_opt_in";
+import { buildUserProfile } from './user_profile_registry';
export async function replaceInjectedVars(originalInjectedVars, request, server) {
const xpackInfo = server.plugins.xpack_main.info;
const withXpackInfo = async () => ({
...originalInjectedVars,
telemetryOptedIn: await getTelemetryOptIn(request),
- xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined
+ xpackInitialInfo: xpackInfo.isAvailable() ? xpackInfo.toJSON() : undefined,
+ userProfile: await buildUserProfile(request),
});
// security feature is disabled
diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts
new file mode 100644
index 0000000000000..22a0b58b60c2a
--- /dev/null
+++ b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.test.ts
@@ -0,0 +1,36 @@
+/*
+ * 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 {
+ buildUserProfile,
+ registerUserProfileCapabilityFactory,
+ removeAllFactories,
+} from './user_profile_registry';
+
+describe('UserProfileRegistry', () => {
+ beforeEach(() => removeAllFactories());
+
+ it('should produce an empty user profile', async () => {
+ expect(await buildUserProfile(null)).toEqual({});
+ });
+
+ it('should accumulate the results of all registered factories', async () => {
+ registerUserProfileCapabilityFactory(async () => ({
+ foo: true,
+ bar: false,
+ }));
+
+ registerUserProfileCapabilityFactory(async () => ({
+ anotherCapability: true,
+ }));
+
+ expect(await buildUserProfile(null)).toEqual({
+ foo: true,
+ bar: false,
+ anotherCapability: true,
+ });
+ });
+});
diff --git a/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts
new file mode 100644
index 0000000000000..417341165fde4
--- /dev/null
+++ b/x-pack/plugins/xpack_main/server/lib/user_profile_registry.ts
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+export type CapabilityFactory = (request: any) => Promise<{ [capability: string]: boolean }>;
+
+let factories: CapabilityFactory[] = [];
+
+export function removeAllFactories() {
+ factories = [];
+}
+
+export function registerUserProfileCapabilityFactory(factory: CapabilityFactory) {
+ factories.push(factory);
+}
+
+export async function buildUserProfile(request: any) {
+ const factoryPromises = factories.map(async factory => ({
+ ...(await factory(request)),
+ }));
+
+ const factoryResults = await Promise.all(factoryPromises);
+
+ return factoryResults.reduce((acc, capabilities) => {
+ return {
+ ...acc,
+ ...capabilities,
+ };
+ }, {});
+}