diff --git a/test/functional/services/dashboard/add_panel.js b/test/functional/services/dashboard/add_panel.js
index 64df0bf612e17..c0f571125d6ea 100644
--- a/test/functional/services/dashboard/add_panel.js
+++ b/test/functional/services/dashboard/add_panel.js
@@ -164,6 +164,8 @@ export function DashboardAddPanelProvider({ getService, getPageObjects }) {
async addVisualization(vizName) {
log.debug(`DashboardAddPanel.addVisualization(${vizName})`);
await this.ensureAddPanelIsShowing();
+ // workaround for timing issue with slideout animation
+ await PageObjects.common.sleep(500);
await this.filterEmbeddableNames(`"${vizName.replace('-', ' ')}"`);
await testSubjects.click(`addPanel${vizName.split(' ').join('-')}`);
await this.closeAddPanel();
diff --git a/test/functional/services/test_subjects.js b/test/functional/services/test_subjects.js
index f40600e71128b..9a614a1acbdc2 100644
--- a/test/functional/services/test_subjects.js
+++ b/test/functional/services/test_subjects.js
@@ -101,9 +101,7 @@ export function TestSubjectsProvider({ getService }) {
async setValue(selector, text) {
return await retry.try(async () => {
- const element = await this.find(selector);
- await element.click();
-
+ await this.click(selector);
// in case the input element is actually a child of the testSubject, we
// call clearValue() and type() on the element that is focused after
// clicking on the testSubject
diff --git a/x-pack/plugins/security/public/components/management/users/confirm_delete.js b/x-pack/plugins/security/public/components/management/users/confirm_delete.js
new file mode 100644
index 0000000000000..53d9bd3bed127
--- /dev/null
+++ b/x-pack/plugins/security/public/components/management/users/confirm_delete.js
@@ -0,0 +1,58 @@
+/*
+ * 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 React, { Component, Fragment } from 'react';
+import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+export class ConfirmDelete extends Component {
+ deleteUsers = () => {
+ const { usersToDelete, apiClient, callback } = this.props;
+ const errors = [];
+ usersToDelete.forEach(async username => {
+ try {
+ await apiClient.deleteUser(username);
+ toastNotifications.addSuccess(`Deleted user ${username}`);
+ } catch (e) {
+ errors.push(username);
+ toastNotifications.addDanger(`Error deleting user ${username}`);
+ }
+ if (callback) {
+ callback(usersToDelete, errors);
+ }
+ });
+ };
+ render() {
+ const { usersToDelete, onCancel } = this.props;
+ const moreThanOne = usersToDelete.length > 1;
+ const title = moreThanOne
+ ? `Delete ${usersToDelete.length} users`
+ : `Delete user '${usersToDelete[0]}'`;
+ return (
+
+
+
+ {moreThanOne ? (
+
+
+ You are about to delete these users:
+
+ {usersToDelete.map(username => - {username}
)}
+
+ ) : null}
+
This operation cannot be undone.
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/security/public/components/management/users/edit_user.js b/x-pack/plugins/security/public/components/management/users/edit_user.js
new file mode 100644
index 0000000000000..390172fa6db32
--- /dev/null
+++ b/x-pack/plugins/security/public/components/management/users/edit_user.js
@@ -0,0 +1,492 @@
+/*
+ * 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.
+ */
+/* eslint camelcase: 0 */
+import React, { Component, Fragment } from 'react';
+import {
+ EuiButton,
+ EuiCallOut,
+ EuiButtonEmpty,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiTitle,
+ EuiForm,
+ EuiFormRow,
+ EuiIcon,
+ EuiText,
+ EuiFieldText,
+ EuiPage,
+ EuiComboBox,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiPageContentBody,
+ EuiHorizontalRule,
+ EuiSpacer,
+} from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+import { USERS_PATH } from '../../../views/management/management_urls';
+import { ConfirmDelete } from './confirm_delete';
+const validEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; //eslint-disable-line max-len
+const validUsernameRegex = /[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*/;
+export class EditUser extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isNewUser: true,
+ currentUser: {},
+ showDeleteConfirmation: false,
+ user: {
+ email: null,
+ username: null,
+ full_name: null,
+ roles: [],
+ },
+ roles: [],
+ selectedRoles: [],
+ password: null,
+ confirmPassword: null,
+ };
+ }
+ async componentDidMount() {
+ const { apiClient, username } = this.props;
+ let { user, currentUser } = this.state;
+ if (username) {
+ user = await apiClient.getUser(username);
+ currentUser = await apiClient.getCurrentUser();
+ }
+ const roles = await apiClient.getRoles();
+ this.setState({
+ isNewUser: !username,
+ currentUser,
+ user,
+ roles,
+ selectedRoles: user.roles.map(role => ({ label: role })) || [],
+ });
+ }
+ handleDelete = (usernames, errors) => {
+ if (errors.length === 0) {
+ const { changeUrl } = this.props;
+ changeUrl(USERS_PATH);
+ }
+ };
+ passwordError = () => {
+ const { password } = this.state;
+ if (password !== null && password.length < 6) {
+ return 'Password must be at least 6 characters';
+ }
+ };
+ currentPasswordError = () => {
+ const { currentPasswordError } = this.state;
+ if (currentPasswordError) {
+ return 'The current password you entered is incorrect';
+ }
+ };
+ confirmPasswordError = () => {
+ const { password, confirmPassword } = this.state;
+ if (password && confirmPassword !== null && password !== confirmPassword) {
+ return 'Passwords do not match';
+ }
+ };
+ usernameError = () => {
+ const { username } = this.state.user;
+ if (username !== null && !username) {
+ return 'Username is required';
+ } else if (username && !username.match(validUsernameRegex)) {
+ return 'Username must begin with a letter or underscore and contain only letters, underscores, and numbers';
+ }
+ };
+ fullnameError = () => {
+ const { full_name } = this.state.user;
+ if (full_name !== null && !full_name) {
+ return 'Full name is required';
+ }
+ };
+ emailError = () => {
+ const { email } = this.state.user;
+ if (email !== null && (!email || !email.match(validEmailRegex))) {
+ return 'A valid email address is required';
+ }
+ };
+ changePassword = async () => {
+ const { apiClient } = this.props;
+ const { user, password, currentPassword } = this.state;
+ try {
+ await apiClient.changePassword(user.username, password, currentPassword);
+ toastNotifications.addSuccess('Password changed.');
+ } catch (e) {
+ if (e.status === 401) {
+ return this.setState({ currentPasswordError: true });
+ } else {
+ toastNotifications.addDanger(`Error setting password: ${e.data.message}`);
+ }
+ }
+ this.clearPasswordForm();
+ };
+ saveUser = async () => {
+ const { apiClient, changeUrl } = this.props;
+ const { user, password, selectedRoles } = this.state;
+ const userToSave = { ...user };
+ userToSave.roles = selectedRoles.map(selectedRole => {
+ return selectedRole.label;
+ });
+ if (password) {
+ userToSave.password = password;
+ }
+ try {
+ await apiClient.saveUser(userToSave);
+ toastNotifications.addSuccess(`Saved user ${user.username}`);
+ changeUrl(USERS_PATH);
+ } catch (e) {
+ toastNotifications.addDanger(`Error saving user: ${e.data.message}`);
+ }
+ };
+ clearPasswordForm = () => {
+ this.setState({
+ showChangePasswordForm: false,
+ password: null,
+ confirmPassword: null,
+ });
+ };
+ passwordFields = () => {
+ const { user, currentUser } = this.state;
+ const userIsLoggedInUser = user.username && user.username === currentUser.username;
+ return (
+
+ {userIsLoggedInUser ? (
+
+ this.setState({ currentPassword: event.target.value })}
+ />
+
+ ) : null}
+
+ this.setState({ password: event.target.value })}
+ onBlur={event => this.setState({ password: event.target.value || '' })}
+ />
+
+
+ this.setState({ confirmPassword: event.target.value })}
+ onBlur={event => this.setState({ confirmPassword: event.target.value || '' })}
+ name="confirm_password"
+ type="password"
+ />
+
+
+ );
+ };
+ changePasswordForm = () => {
+ const {
+ showChangePasswordForm,
+ password,
+ confirmPassword,
+ user: { username },
+ } = this.state;
+ if (!showChangePasswordForm) {
+ return null;
+ }
+ return (
+
+
+ {this.passwordFields()}
+ {username === 'kibana' ? (
+
+
+
+ After you change the password for the kibana user, you must update the kibana.yml
+ file and restart Kibana.
+
+
+
+
+ ) : null}
+
+
+ {
+ this.changePassword(password);
+ }}
+ >
+ Save password
+
+
+
+ {
+ this.clearPasswordForm();
+ }}
+ >
+ Cancel
+
+
+
+
+ );
+ };
+ toggleChangePasswordForm = () => {
+ const { showChangePasswordForm } = this.state;
+ this.setState({ showChangePasswordForm: !showChangePasswordForm });
+ };
+ onRolesChange = selectedRoles => {
+ this.setState({
+ selectedRoles,
+ });
+ };
+ cannotSaveUser = () => {
+ const { user, isNewUser } = this.state;
+ return (
+ !user.username ||
+ !user.full_name ||
+ !user.email ||
+ this.emailError() ||
+ (isNewUser && (this.passwordError() || this.confirmPasswordError()))
+ );
+ };
+ onCancelDelete = () => {
+ this.setState({ showDeleteConfirmation: false });
+ };
+ render() {
+ const { changeUrl, apiClient } = this.props;
+ const {
+ user,
+ roles,
+ selectedRoles,
+ showChangePasswordForm,
+ isNewUser,
+ showDeleteConfirmation,
+ } = this.state;
+ const reserved = user.metadata && user.metadata._reserved;
+ if (!user || !roles) {
+ return null;
+ }
+ return (
+
+
+
+
+
+
+ {isNewUser ? 'New user' : `Edit "${user.username}" user`}
+
+
+ {reserved && (
+
+
+
+ )}
+
+
+ {reserved && (
+
+
+ Reserved users are built-in and cannot be removed or modified. Only the password
+ may be changed.
+
+
+ )}
+
+ {showDeleteConfirmation ? (
+
+ ) : null}
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/security/public/components/management/users/index.js b/x-pack/plugins/security/public/components/management/users/index.js
new file mode 100644
index 0000000000000..4374ff1b9d097
--- /dev/null
+++ b/x-pack/plugins/security/public/components/management/users/index.js
@@ -0,0 +1,11 @@
+/*
+ * 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.
+ */
+
+/*! 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 { Users } from './users';
+export { EditUser } from './edit_user';
diff --git a/x-pack/plugins/security/public/components/management/users/users.js b/x-pack/plugins/security/public/components/management/users/users.js
new file mode 100644
index 0000000000000..2a14b99aef975
--- /dev/null
+++ b/x-pack/plugins/security/public/components/management/users/users.js
@@ -0,0 +1,243 @@
+/*
+ * 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 React, { Component, Fragment } from 'react';
+import {
+ EuiButton,
+ EuiIcon,
+ EuiLink,
+ EuiInMemoryTable,
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiTitle,
+ EuiPageContentHeader,
+ EuiPageContentHeaderSection,
+ EuiPageContentBody,
+ EuiEmptyPrompt,
+} from '@elastic/eui';
+import { toastNotifications } from 'ui/notify';
+import { ConfirmDelete } from './confirm_delete';
+
+export class Users extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ users: [],
+ selection: [],
+ showDeleteConfirmation: false,
+ };
+ }
+ componentDidMount() {
+ this.loadUsers();
+ }
+ handleDelete = (usernames, errors) => {
+ const { users } = this.state;
+ this.setState({
+ selection: [],
+ showDeleteConfirmation: false,
+ users: users.filter(({ username }) => {
+ return !usernames.includes(username) || errors.includes(username);
+ }),
+ });
+ };
+ async loadUsers() {
+ const { apiClient } = this.props;
+ try {
+ const users = await apiClient.getUsers();
+ this.setState({ users });
+ } catch (e) {
+ if (e.status === 403) {
+ this.setState({ permissionDenied: true });
+ } else {
+ toastNotifications.addDanger(`Error fetching users: ${e.data.message}`);
+ }
+ }
+ }
+ renderToolsLeft() {
+ const { selection } = this.state;
+ if (selection.length === 0) {
+ return;
+ }
+ const numSelected = selection.length;
+ return (
+ this.setState({ showDeleteConfirmation: true })}
+ >
+ Delete {numSelected} user{numSelected > 1 ? 's' : ''}
+
+ );
+ }
+ onCancelDelete = () => {
+ this.setState({ showDeleteConfirmation: false });
+ }
+ render() {
+ const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state;
+ const { apiClient } = this.props;
+ if (permissionDenied) {
+ return (
+
+
+
+ Permission denied}
+ body={You do not have permission to manage users.
}
+ />
+
+
+
+ );
+ }
+ const path = '#/management/security/';
+ const columns = [
+ {
+ field: 'full_name',
+ name: 'Full Name',
+ sortable: true,
+ truncateText: true,
+ render: fullName => {
+ return
{fullName}
;
+ },
+ },
+ {
+ field: 'username',
+ name: 'User Name',
+ sortable: true,
+ truncateText: true,
+ render: username => (
+
+ {username}
+
+ ),
+ },
+ {
+ field: 'email',
+ name: 'Email Address',
+ sortable: true,
+ truncateText: true,
+ },
+ {
+ field: 'roles',
+ name: 'Roles',
+ render: rolenames => {
+ const roleLinks = rolenames.map((rolename, index) => {
+ return (
+
+ {rolename}
+ {index === rolenames.length - 1 ? null : ', '}
+
+ );
+ });
+ return {roleLinks}
;
+ },
+ },
+ {
+ field: 'metadata._reserved',
+ name: 'Reserved',
+ sortable: false,
+ width: '100px',
+ align: 'right',
+ description:
+ 'Reserved users are built-in and cannot be removed. Only the password can be changed.',
+ render: reserved =>
+ reserved ? (
+
+ ) : null,
+ },
+ ];
+ const pagination = {
+ initialPageSize: 20,
+ pageSizeOptions: [10, 20, 50, 100],
+ };
+
+ const selectionConfig = {
+ itemId: 'username',
+ selectable: user => !user.metadata._reserved,
+ selectableMessage: selectable => (!selectable ? 'User is a system user' : undefined),
+ onSelectionChange: selection => this.setState({ selection }),
+ };
+ const search = {
+ toolsLeft: this.renderToolsLeft(),
+ box: {
+ incremental: true,
+ },
+ onChange: query => {
+ this.setState({
+ filter: query.queryText,
+ });
+ },
+ };
+ const sorting = {
+ sort: {
+ field: 'full_name',
+ direction: 'asc',
+ },
+ };
+ const rowProps = () => {
+ return {
+ 'data-test-subj': 'userRow',
+ };
+ };
+ const usersToShow = filter
+ ? users.filter(({ username, roles }) => {
+ const normalized = `${username} ${roles.join(' ')}`.toLowerCase();
+ const normalizedQuery = filter.toLowerCase();
+ return normalized.indexOf(normalizedQuery) !== -1;
+ }) : users;
+ return (
+
+
+
+
+
+
+ Users
+
+
+
+
+ Create new user
+
+
+
+
+
+ {showDeleteConfirmation ? (
+ user.username)}
+ callback={this.handleDelete}
+ />
+ ) : null}
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/x-pack/plugins/security/public/lib/api.js b/x-pack/plugins/security/public/lib/api.js
new file mode 100644
index 0000000000000..908a0ceaf3103
--- /dev/null
+++ b/x-pack/plugins/security/public/lib/api.js
@@ -0,0 +1,55 @@
+/*
+ * 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 chrome from 'ui/chrome';
+
+const usersUrl = chrome.addBasePath('/api/security/v1/users');
+const rolesUrl = chrome.addBasePath('/api/security/v1/roles');
+
+export const createApiClient = (httpClient) => {
+ return {
+ async getCurrentUser() {
+ const url = chrome.addBasePath('/api/security/v1/me');
+ const { data } = await httpClient.get(url);
+ return data;
+ },
+ async getUsers() {
+ const { data } = await httpClient.get(usersUrl);
+ return data;
+ },
+ async getUser(username) {
+ const url = `${usersUrl}/${username}`;
+ const { data } = await httpClient.get(url);
+ return data;
+ },
+ async deleteUser(username) {
+ const url = `${usersUrl}/${username}`;
+ await httpClient.delete(url);
+ },
+ async saveUser(user) {
+ const url = `${usersUrl}/${user.username}`;
+ await httpClient.post(url, user);
+ },
+ async getRoles() {
+ const { data } = await httpClient.get(rolesUrl);
+ return data;
+ },
+ async getRole(name) {
+ const url = `${rolesUrl}/${name}`;
+ const { data } = await httpClient.get(url);
+ return data;
+ },
+ async changePassword(username, password, currentPassword) {
+ const data = {
+ newPassword: password,
+ };
+ if (currentPassword) {
+ data.password = currentPassword;
+ }
+ await httpClient
+ .post(`${usersUrl}/${username}/password`, data);
+ }
+ };
+};
diff --git a/x-pack/plugins/security/public/views/management/edit_user.html b/x-pack/plugins/security/public/views/management/edit_user.html
index be4ef416ce84c..b74b08b0e4ae2 100644
--- a/x-pack/plugins/security/public/views/management/edit_user.html
+++ b/x-pack/plugins/security/public/views/management/edit_user.html
@@ -1,184 +1,3 @@
-
-
-
-
-
-
-
- Add user
-
-
- “{{ user.username }}” User
-
-
-
-
-
-
-
-
-
- Reserved
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/x-pack/plugins/security/public/views/management/edit_user.js b/x-pack/plugins/security/public/views/management/edit_user.js
index abe421405c2c1..5ba229605e4c7 100644
--- a/x-pack/plugins/security/public/views/management/edit_user.js
+++ b/x-pack/plugins/security/public/views/management/edit_user.js
@@ -3,101 +3,47 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-
-import _ from 'lodash';
import routes from 'ui/routes';
-import { fatalError, toastNotifications } from 'ui/notify';
import template from 'plugins/security/views/management/edit_user.html';
import 'angular-resource';
import 'angular-ui-select';
import 'plugins/security/services/shield_user';
import 'plugins/security/services/shield_role';
-import { checkLicenseError } from 'plugins/security/lib/check_license_error';
-import { EDIT_USERS_PATH, USERS_PATH } from './management_urls';
+import { EDIT_USERS_PATH } from './management_urls';
+import { EditUser } from '../../components/management/users';
+import React from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
+import { createApiClient } from '../../lib/api';
+
+const renderReact = (elem, httpClient, changeUrl, username) => {
+ render(
+ ,
+ elem
+ );
+};
routes.when(`${EDIT_USERS_PATH}/:username?`, {
template,
- resolve: {
- me(ShieldUser) {
- return ShieldUser.getCurrent();
- },
-
- user($route, ShieldUser, kbnUrl, Promise) {
- const username = $route.current.params.username;
- if (username != null) {
- return ShieldUser.get({ username }).$promise
- .catch((response) => {
- if (response.status !== 404) {
- return fatalError(response);
- }
-
- toastNotifications.addDanger(`No "${username}" user found.`);
- kbnUrl.redirect(USERS_PATH);
- return Promise.halt();
- });
- }
- return new ShieldUser({ roles: [] });
- },
-
- roles(ShieldRole, kbnUrl, Promise, Private) {
- // $promise is used here because the result is an ngResource, not a promise itself
- return ShieldRole.query().$promise
- .then((roles) => _.map(roles, 'name'))
- .catch(checkLicenseError(kbnUrl, Promise, Private));
- }
- },
controllerAs: 'editUser',
- controller($scope, $route, kbnUrl, Notifier, confirmModal) {
- $scope.me = $route.current.locals.me;
- $scope.user = $route.current.locals.user;
- $scope.availableRoles = $route.current.locals.roles;
- $scope.usersHref = `#${USERS_PATH}`;
-
- this.isNewUser = $route.current.params.username == null;
-
- $scope.deleteUser = (user) => {
- const doDelete = () => {
- user.$delete()
- .then(() => toastNotifications.addSuccess('Deleted user'))
- .then($scope.goToUserList)
- .catch(error => toastNotifications.addDanger(_.get(error, 'data.message')));
- };
- const confirmModalOptions = {
- confirmButtonText: 'Delete user',
- onConfirm: doDelete
- };
- confirmModal('Are you sure you want to delete this user? This action is irreversible!', confirmModalOptions);
- };
-
- $scope.saveUser = (user) => {
- // newPassword is unexepcted by the API.
- delete user.newPassword;
- user.$save()
- .then(() => toastNotifications.addSuccess('User updated'))
- .then($scope.goToUserList)
- .catch(error => toastNotifications.addDanger(_.get(error, 'data.message')));
- };
-
- $scope.goToUserList = () => {
- kbnUrl.redirect(USERS_PATH);
- };
-
- $scope.saveNewPassword = (newPassword, currentPassword, onSuccess, onIncorrectPassword) => {
- $scope.user.newPassword = newPassword;
- if (currentPassword) {
- // If the currentPassword is null, we shouldn't send it.
- $scope.user.password = currentPassword;
+ controller($scope, $route, kbnUrl, Notifier, confirmModal, $http) {
+ $scope.$on('$destroy', () => {
+ const elem = document.getElementById('editUserReactRoot');
+ if (elem) {
+ unmountComponentAtNode(elem);
}
-
- $scope.user.$changePassword()
- .then(() => toastNotifications.addSuccess('Password updated'))
- .then(onSuccess)
- .catch(error => {
- if (error.status === 401) {
- onIncorrectPassword();
- }
- else toastNotifications.addDanger(_.get(error, 'data.message'));
- });
- };
- }
+ });
+ $scope.$$postDigest(() => {
+ const elem = document.getElementById('editUserReactRoot');
+ const username = $route.current.params.username;
+ const changeUrl = (url) => {
+ kbnUrl.change(url);
+ $scope.$apply();
+ };
+ renderReact(elem, $http, changeUrl, username);
+ });
+ },
});
diff --git a/x-pack/plugins/security/public/views/management/management.less b/x-pack/plugins/security/public/views/management/management.less
index d9e16650aeecc..046f4b7eb8c53 100644
--- a/x-pack/plugins/security/public/views/management/management.less
+++ b/x-pack/plugins/security/public/views/management/management.less
@@ -10,3 +10,19 @@
margin-left: 10px;
}
}
+
+.mgtUsersEditPage,
+.mgtUsersListingPage {
+ min-height: ~"calc(100vh - 70px)";
+}
+
+.mgtUsersListingPage__content {
+ flex-grow: 0;
+}
+
+.mgtUsersEditPage__content {
+ max-width: 460px;
+ margin-left: auto;
+ margin-right: auto;
+ flex-grow: 0;
+}
diff --git a/x-pack/plugins/security/public/views/management/users.html b/x-pack/plugins/security/public/views/management/users.html
index 3ab259faf9c02..83efad0ae6a69 100644
--- a/x-pack/plugins/security/public/views/management/users.html
+++ b/x-pack/plugins/security/public/views/management/users.html
@@ -1,251 +1,3 @@
-
-
-
-
-
-
- Please contact your administrator.
-
-
-
-
-
-
-
-
-
-
-
-
-
- No users match your search criteria
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/x-pack/plugins/security/public/views/management/users.js b/x-pack/plugins/security/public/views/management/users.js
index 99351f1639c63..fdbb115751907 100644
--- a/x-pack/plugins/security/public/views/management/users.js
+++ b/x-pack/plugins/security/public/views/management/users.js
@@ -4,89 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import _ from 'lodash';
+import React from 'react';
+import { render, unmountComponentAtNode } from 'react-dom';
import routes from 'ui/routes';
-import { toastNotifications } from 'ui/notify';
-import { toggle, toggleSort } from 'plugins/security/lib/util';
import template from 'plugins/security/views/management/users.html';
import 'plugins/security/services/shield_user';
-import { checkLicenseError } from 'plugins/security/lib/check_license_error';
-import { SECURITY_PATH, USERS_PATH, EDIT_USERS_PATH, EDIT_ROLES_PATH } from './management_urls';
-
+import { SECURITY_PATH, USERS_PATH } from './management_urls';
+import { Users } from '../../components/management/users';
+import { createApiClient } from '../../lib/api';
routes.when(SECURITY_PATH, {
- redirectTo: USERS_PATH
+ redirectTo: USERS_PATH,
});
+const renderReact = (elem, httpClient, changeUrl) => {
+ render(, elem);
+};
+
routes.when(USERS_PATH, {
template,
- resolve: {
- users(ShieldUser, kbnUrl, Promise, Private) {
- // $promise is used here because the result is an ngResource, not a promise itself
- return ShieldUser.query().$promise
- .catch(checkLicenseError(kbnUrl, Promise, Private))
- .catch(_.identity); // Return the error if there is one
- }
- },
-
- controller($scope, $route, $q, confirmModal) {
- $scope.users = $route.current.locals.users;
- $scope.forbidden = !_.isArray($scope.users);
- $scope.selectedUsers = [];
- $scope.sort = { orderBy: 'full_name', reverse: false };
- $scope.editUsersHref = `#${EDIT_USERS_PATH}`;
- $scope.getEditUrlHref = (user) => `#${EDIT_USERS_PATH}/${user}`;
- $scope.getEditRoleHref = (role) => `#${EDIT_ROLES_PATH}/${role}`;
-
- $scope.deleteUsers = () => {
- const doDelete = () => {
- $q.all($scope.selectedUsers.map((user) => user.$delete()))
- .then(() => toastNotifications.addSuccess(`Deleted ${$scope.selectedUsers.length > 1 ? 'users' : 'user'}`))
- .then(() => {
- $scope.selectedUsers.map((user) => {
- const i = $scope.users.indexOf(user);
- $scope.users.splice(i, 1);
- });
- $scope.selectedUsers.length = 0;
- });
- };
- const confirmModalOptions = {
- onConfirm: doDelete,
- confirmButtonText: 'Delete user(s)'
+ controller($scope, $route, $q, confirmModal, $http, kbnUrl) {
+ $scope.$on('$destroy', () => {
+ const elem = document.getElementById('usersReactRoot');
+ if (elem) unmountComponentAtNode(elem);
+ });
+ $scope.$$postDigest(() => {
+ const elem = document.getElementById('usersReactRoot');
+ const changeUrl = (url) => {
+ kbnUrl.change(url);
+ $scope.$apply();
};
- confirmModal(
- 'Are you sure you want to delete the selected user(s)? This action is irreversible!',
- confirmModalOptions
- );
- };
-
- $scope.getSortArrowClass = field => {
- if ($scope.sort.orderBy === field) {
- return $scope.sort.reverse ? 'fa-long-arrow-down' : 'fa-long-arrow-up';
- }
-
- // Sort ascending by default.
- return 'fa-long-arrow-up';
- };
-
- $scope.toggleAll = () => {
- if ($scope.allSelected()) {
- $scope.selectedUsers.length = 0;
- } else {
- $scope.selectedUsers = getActionableUsers().slice();
- }
- };
-
- $scope.allSelected = () => {
- const users = getActionableUsers();
- return users.length && users.length === $scope.selectedUsers.length;
- };
-
- $scope.toggle = toggle;
- $scope.includes = _.includes;
- $scope.toggleSort = toggleSort;
-
- function getActionableUsers() {
- return $scope.users.filter((user) => !user.metadata._reserved);
- }
- }
+ renderReact(elem, $http, changeUrl);
+ });
+ },
});
diff --git a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js
index 417ff83a5800f..7fb26877a4a6c 100644
--- a/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js
+++ b/x-pack/test/functional/apps/dashboard_mode/dashboard_view_mode.js
@@ -57,12 +57,11 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.navigateTo();
await PageObjects.security.clickUsersSection();
await PageObjects.security.clickCreateNewUser();
-
await testSubjects.setValue('userFormUserNameInput', 'dashuser');
await testSubjects.setValue('passwordInput', '123456');
await testSubjects.setValue('passwordConfirmationInput', '123456');
await testSubjects.setValue('userFormFullNameInput', 'dashuser');
- await testSubjects.setValue('userFormEmailInput', 'my@email.com');
+ await testSubjects.setValue('userFormEmailInput', 'example@example.com');
await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user');
await PageObjects.security.assignRoleToUser('logstash-data');
@@ -76,7 +75,7 @@ export default function ({ getService, getPageObjects }) {
await testSubjects.setValue('passwordInput', '123456');
await testSubjects.setValue('passwordConfirmationInput', '123456');
await testSubjects.setValue('userFormFullNameInput', 'mixeduser');
- await testSubjects.setValue('userFormEmailInput', 'my@email.com');
+ await testSubjects.setValue('userFormEmailInput', 'example@example.com');
await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user');
await PageObjects.security.assignRoleToUser('kibana_user');
await PageObjects.security.assignRoleToUser('logstash-data');
@@ -91,7 +90,7 @@ export default function ({ getService, getPageObjects }) {
await testSubjects.setValue('passwordInput', '123456');
await testSubjects.setValue('passwordConfirmationInput', '123456');
await testSubjects.setValue('userFormFullNameInput', 'mixeduser');
- await testSubjects.setValue('userFormEmailInput', 'my@email.com');
+ await testSubjects.setValue('userFormEmailInput', 'example@example.com');
await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user');
await PageObjects.security.assignRoleToUser('superuser');
diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js
index d01d5e2b99d5d..b2cfb1898b9c6 100644
--- a/x-pack/test/functional/apps/security/management.js
+++ b/x-pack/test/functional/apps/security/management.js
@@ -53,7 +53,7 @@ export default function ({ getService, getPageObjects }) {
await testSubjects.setValue('passwordInput', '123456');
await testSubjects.setValue('passwordConfirmationInput', '123456');
await testSubjects.setValue('userFormFullNameInput', 'Full User Name');
- await testSubjects.setValue('userFormEmailInput', 'my@email.com');
+ await testSubjects.setValue('userFormEmailInput', 'example@example.com');
await PageObjects.security.clickSaveEditUser();
@@ -66,8 +66,9 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.settings.clickLinkText('new-user');
const currentUrl = await remote.getCurrentUrl();
expect(currentUrl).to.contain(EDIT_USERS_PATH);
-
const userNameInput = await testSubjects.find('userFormUserNameInput');
+ // allow time for user to load
+ await PageObjects.common.sleep(500);
const userName = await userNameInput.getProperty('value');
expect(userName).to.equal('new-user');
});
@@ -122,7 +123,7 @@ export default function ({ getService, getPageObjects }) {
await testSubjects.setValue('passwordInput', '123456');
await testSubjects.setValue('passwordConfirmationInput', '123456');
await testSubjects.setValue('userFormFullNameInput', 'dashuser');
- await testSubjects.setValue('userFormEmailInput', 'my@email.com');
+ await testSubjects.setValue('userFormEmailInput', 'example@example.com');
await PageObjects.security.assignRoleToUser('kibana_dashboard_only_user');
await PageObjects.security.assignRoleToUser('logstash-data');
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 318604406b1fd..fd64431b3ca7f 100644
--- a/x-pack/test/functional/apps/security/secure_roles_perm.js
+++ b/x-pack/test/functional/apps/security/secure_roles_perm.js
@@ -13,6 +13,8 @@ export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const remote = getService('remote');
const kibanaServer = getService('kibanaServer');
+ const testSubjects = getService('testSubjects');
+ const retry = getService('retry');
@@ -64,12 +66,12 @@ export default function ({ getService, getPageObjects }) {
});
- it('Kibana User navigating to Management gets - You do not have permission to manage users', async function () {
- const expectedMessage = 'You do not have permission to manage users.';
+ it('Kibana User navigating to Management gets permission denied', async function () {
await PageObjects.settings.navigateTo();
await PageObjects.security.clickElasticsearchUsers();
- const actualMessage = await PageObjects.security.getPermissionDeniedMessage();
- expect(actualMessage).to.be(expectedMessage);
+ await retry.tryForTime(2000, async () => {
+ await testSubjects.find('permissionDeniedMessage');
+ });
});
it('Kibana User navigating to Discover and trying to generate CSV gets - Authorization Error ', async function () {
diff --git a/x-pack/test/functional/page_objects/security_page.js b/x-pack/test/functional/page_objects/security_page.js
index c2e45fcdf9215..61ebe75492ab5 100644
--- a/x-pack/test/functional/page_objects/security_page.js
+++ b/x-pack/test/functional/page_objects/security_page.js
@@ -117,9 +117,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
}
async clickSaveEditUser() {
- const saveButton = await retry.try(() => testSubjects.find('userFormSaveButton'));
- await remote.moveMouseTo(saveButton);
- await saveButton.click();
+ await testSubjects.click('userFormSaveButton');
await PageObjects.header.waitUntilLoadingHasFinished();
}
@@ -146,11 +144,7 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
}
async assignRoleToUser(role) {
- log.debug(`Adding role ${role} to user`);
- const privilegeInput =
- await retry.try(() => find.byCssSelector('[data-test-subj="userFormRolesDropdown"] > div > input'));
- await privilegeInput.type(role);
- await privilegeInput.type('\n');
+ await this.selectRole(role);
}
async navigateTo() {
@@ -182,13 +176,18 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
const fullnameElement = await user.findByCssSelector('[data-test-subj="userRowFullName"]');
const usernameElement = await user.findByCssSelector('[data-test-subj="userRowUserName"]');
const rolesElement = await user.findByCssSelector('[data-test-subj="userRowRoles"]');
- const isReservedElementVisible = await user.findByCssSelector('td:nth-child(5)');
+ let reserved = false;
+ try {
+ reserved = !!(await user.findByCssSelector('[data-test-subj="reservedUser"]'));
+ } catch(e) {
+ //ignoring, just means user is not reserved
+ }
return {
username: await usernameElement.getVisibleText(),
fullname: await fullnameElement.getVisibleText(),
roles: (await rolesElement.getVisibleText()).split(',').map(role => role.trim()),
- reserved: (await isReservedElementVisible.getProperty('innerHTML')).includes('userRowReserved')
+ reserved
};
});
}
@@ -221,23 +220,12 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
await testSubjects.setValue('passwordInput', userObj.password);
await testSubjects.setValue('passwordConfirmationInput', userObj.confirmPassword);
await testSubjects.setValue('userFormFullNameInput', userObj.fullname);
- await testSubjects.setValue('userFormEmailInput', userObj.email);
-
- function addRoles(role) {
- return role.reduce(function (promise, roleName) {
- return promise
- .then(function () {
- log.debug('Add role: ' + roleName);
- return self.selectRole(roleName);
- })
- .then(function () {
- return PageObjects.common.sleep(1000);
- });
-
- }, Promise.resolve());
- }
+ await testSubjects.setValue('userFormEmailInput', 'example@example.com');
log.debug('Add roles: ', userObj.roles);
- await addRoles(userObj.roles || []);
+ const rolesToAdd = userObj.roles || [];
+ for (let i = 0; i < rolesToAdd.length; i++) {
+ await self.selectRole(rolesToAdd[i]);
+ }
log.debug('After Add role: , userObj.roleName');
if (userObj.save === true) {
await testSubjects.click('userFormSaveButton');
@@ -337,8 +325,9 @@ export function SecurityPageProvider({ getService, getPageObjects }) {
const dropdown = await testSubjects.find("userFormRolesDropdown");
const input = await dropdown.findByCssSelector("input");
await input.type(role);
- await testSubjects.click(`addRoleOption-${role}`);
- await testSubjects.find(`userRole-${role}`);
+ await testSubjects.click(`roleOption-${role}`);
+ await testSubjects.click('comboBoxToggleListButton');
+ await testSubjects.find(`roleOption-${role}`);
}
deleteUser(username) {