Reactify users roles (#20739) (#21028)

* partial progress on reactifying users

* progress on EUIfication of users screen

* removing Angular stuff

* adding data-test-subj="passwordConfirmationInput"

* removing data-test-subj="userFormEmailInput" refs from tests

* fixing selector for role assignment

* some functional test fixes

* fixing some functional tests

* fixing last functional test

* removing stray console log

* fixing warnings

* attempting to fix flaky test

* trying again to fix flaky test

* PR feedback

* PR feedback

* fixing issue where form tried to submit

* adding sleep to allow user to load

* Design edits

Mainly adding wrapper EUI page elements, but also shifted around form elements.

* Fixed console error and added responsive prop to table

* addressing PR feedback

* A few more PR feedback

- Fixed alignment of table
- Removed the tooltip from the lock icon and placed the description inline.
- Changed delete button to an empty button

* addressing more PR feedback

* adding email field back in

* adding back username validation

* restoring original error message

* fixing dumb null error
This commit is contained in:
Bill McConaghy 2018-07-20 10:15:32 -04:00 committed by GitHub
parent 89799ca6b6
commit 6c5d885c38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 965 additions and 647 deletions

View file

@ -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();

View file

@ -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

View file

@ -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 (
<EuiOverlayMask>
<EuiConfirmModal
title={title}
onCancel={onCancel}
onConfirm={this.deleteUsers}
cancelButtonText="Cancel"
confirmButtonText="Delete"
buttonColor="danger"
>
<div>
{moreThanOne ? (
<Fragment>
<p>
You are about to delete these users:
</p>
<ul>{usersToDelete.map(username => <li key={username}>{username}</li>)}</ul>
</Fragment>
) : null}
<p>This operation cannot be undone.</p>
</div>
</EuiConfirmModal>
</EuiOverlayMask>
);
}
}

View file

@ -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 (
<Fragment>
{userIsLoggedInUser ? (
<EuiFormRow
label="Current password"
isInvalid={!!this.currentPasswordError()}
error={this.currentPasswordError()}
>
<EuiFieldText
name="currentPassword"
type="password"
onChange={event => this.setState({ currentPassword: event.target.value })}
/>
</EuiFormRow>
) : null}
<EuiFormRow
label={userIsLoggedInUser ? 'New password' : 'Password'}
isInvalid={!!this.passwordError()}
error={this.passwordError()}
>
<EuiFieldText
data-test-subj="passwordInput"
name="password"
type="password"
onChange={event => this.setState({ password: event.target.value })}
onBlur={event => this.setState({ password: event.target.value || '' })}
/>
</EuiFormRow>
<EuiFormRow
label="Confirm password"
isInvalid={!!this.confirmPasswordError()}
error={this.confirmPasswordError()}
>
<EuiFieldText
data-test-subj="passwordConfirmationInput"
onChange={event => this.setState({ confirmPassword: event.target.value })}
onBlur={event => this.setState({ confirmPassword: event.target.value || '' })}
name="confirm_password"
type="password"
/>
</EuiFormRow>
</Fragment>
);
};
changePasswordForm = () => {
const {
showChangePasswordForm,
password,
confirmPassword,
user: { username },
} = this.state;
if (!showChangePasswordForm) {
return null;
}
return (
<Fragment>
<EuiHorizontalRule />
{this.passwordFields()}
{username === 'kibana' ? (
<Fragment>
<EuiCallOut title="Extra step needed" color="warning" iconType="help">
<p>
After you change the password for the kibana user, you must update the kibana.yml
file and restart Kibana.
</p>
</EuiCallOut>
<EuiSpacer />
</Fragment>
) : null}
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
size="s"
fill
disabled={
!password || !confirmPassword || this.passwordError() || this.confirmPasswordError()
}
onClick={() => {
this.changePassword(password);
}}
>
Save password
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
onClick={() => {
this.clearPasswordForm();
}}
>
Cancel
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
};
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 (
<EuiPage className="mgtUsersEditPage">
<EuiPageBody>
<EuiPageContent className="mgtUsersEditPage__content">
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>{isNewUser ? 'New user' : `Edit "${user.username}" user`}</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
{reserved && (
<EuiPageContentHeaderSection>
<EuiIcon type="lock" size="l" color="subdued" />
</EuiPageContentHeaderSection>
)}
</EuiPageContentHeader>
<EuiPageContentBody>
{reserved && (
<EuiText size="s" color="subdued">
<p>
Reserved users are built-in and cannot be removed or modified. Only the password
may be changed.
</p>
</EuiText>
)}
{showDeleteConfirmation ? (
<ConfirmDelete
onCancel={this.onCancelDelete}
apiClient={apiClient}
usersToDelete={[user.username]}
callback={this.handleDelete}
/>
) : null}
<form
onSubmit={event => {
event.preventDefault();
}}
>
<EuiForm>
<EuiFormRow
isInvalid={!!this.usernameError()}
error={this.usernameError()}
helpText={
!isNewUser && !reserved
? "Username's cannot be changed after creation."
: null
}
label="Username"
>
<EuiFieldText
onBlur={event =>
this.setState({
user: {
...this.state.user,
username: event.target.value || '',
},
})
}
value={user.username || ''}
name="username"
data-test-subj="userFormUserNameInput"
disabled={!isNewUser}
onChange={event => {
this.setState({
user: { ...this.state.user, username: event.target.value },
});
}}
/>
</EuiFormRow>
{isNewUser ? this.passwordFields() : null}
{reserved ? null : (
<Fragment>
<EuiFormRow
isInvalid={!!this.fullnameError()}
error={this.fullnameError()}
label="Full name"
>
<EuiFieldText
onBlur={event =>
this.setState({
user: {
...this.state.user,
full_name: event.target.value || '',
},
})
}
data-test-subj="userFormFullNameInput"
name="full_name"
value={user.full_name || ''}
onChange={event => {
this.setState({
user: {
...this.state.user,
full_name: event.target.value,
},
});
}}
/>
</EuiFormRow>
<EuiFormRow
isInvalid={!!this.emailError()}
error={this.emailError()}
label="Email address"
>
<EuiFieldText
onBlur={event =>
this.setState({
user: {
...this.state.user,
email: event.target.value || '',
},
})
}
data-test-subj="userFormEmailInput"
name="email"
value={user.email || ''}
onChange={event => {
this.setState({
user: {
...this.state.user,
email: event.target.value,
},
});
}}
/>
</EuiFormRow>
</Fragment>
)}
<EuiFormRow label="Roles">
<EuiComboBox
data-test-subj="userFormRolesDropdown"
placeholder="Add roles"
onChange={this.onRolesChange}
isDisabled={reserved}
name="roles"
options={roles.map(role => {
return { 'data-test-subj': `roleOption-${role.name}`, label: role.name };
})}
selectedOptions={selectedRoles}
/>
</EuiFormRow>
{isNewUser || showChangePasswordForm ? null : (
<EuiFormRow label="Password">
<EuiLink onClick={this.toggleChangePasswordForm}>Change password</EuiLink>
</EuiFormRow>
)}
{this.changePasswordForm()}
<EuiHorizontalRule />
{reserved && (
<EuiButton onClick={() => changeUrl(USERS_PATH)}>Return to user list</EuiButton>
)}
{reserved ? null : (
<EuiFlexGroup responsive={false}>
<EuiFlexItem grow={false}>
<EuiButton
disabled={this.cannotSaveUser()}
fill
data-test-subj="userFormSaveButton"
onClick={() => this.saveUser()}
>
{isNewUser ? 'Create user' : 'Update user'}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="userFormCancelButton"
onClick={() => changeUrl(USERS_PATH)}
>
Cancel
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={true} />
{isNewUser || reserved ? null : (
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={() => {
this.setState({ showDeleteConfirmation: true });
}}
data-test-subj="userFormDeleteButton"
color="danger"
>
Delete user
</EuiButtonEmpty>
</EuiFlexItem>
)}
</EuiFlexGroup>
)}
</EuiForm>
</form>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}

View file

@ -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';

View file

@ -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 (
<EuiButton
data-test-subj="deleteUserButton"
color="danger"
onClick={() => this.setState({ showDeleteConfirmation: true })}
>
Delete {numSelected} user{numSelected > 1 ? 's' : ''}
</EuiButton>
);
}
onCancelDelete = () => {
this.setState({ showDeleteConfirmation: false });
}
render() {
const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state;
const { apiClient } = this.props;
if (permissionDenied) {
return (
<EuiPage className="mgtUsersListingPage">
<EuiPageBody>
<EuiPageContent horizontalPosition="center">
<EuiEmptyPrompt
iconType="securityApp"
iconColor={null}
title={<h2>Permission denied</h2>}
body={<p data-test-subj="permissionDeniedMessage">You do not have permission to manage users.</p>}
/>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
const path = '#/management/security/';
const columns = [
{
field: 'full_name',
name: 'Full Name',
sortable: true,
truncateText: true,
render: fullName => {
return <div data-test-subj="userRowFullName">{fullName}</div>;
},
},
{
field: 'username',
name: 'User Name',
sortable: true,
truncateText: true,
render: username => (
<EuiLink data-test-subj="userRowUserName" href={`${path}users/edit/${username}`}>
{username}
</EuiLink>
),
},
{
field: 'email',
name: 'Email Address',
sortable: true,
truncateText: true,
},
{
field: 'roles',
name: 'Roles',
render: rolenames => {
const roleLinks = rolenames.map((rolename, index) => {
return (
<Fragment key={rolename}>
<EuiLink href={`${path}roles/edit/${rolename}`}>{rolename}</EuiLink>
{index === rolenames.length - 1 ? null : ', '}
</Fragment>
);
});
return <div data-test-subj="userRowRoles">{roleLinks}</div>;
},
},
{
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 ? (
<EuiIcon aria-label="Reserved user" data-test-subj="reservedUser" type="check" />
) : 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 (
<EuiPage className="mgtUsersListingPage">
<EuiPageBody>
<EuiPageContent className="mgtUsersListingPage__content">
<EuiPageContentHeader>
<EuiPageContentHeaderSection>
<EuiTitle>
<h2>Users</h2>
</EuiTitle>
</EuiPageContentHeaderSection>
<EuiPageContentHeaderSection>
<EuiButton
data-test-subj="createUserButton"
href="#/management/security/users/edit"
>
Create new user
</EuiButton>
</EuiPageContentHeaderSection>
</EuiPageContentHeader>
<EuiPageContentBody>
{showDeleteConfirmation ? (
<ConfirmDelete
onCancel={this.onCancelDelete}
apiClient={apiClient}
usersToDelete={selection.map((user) => user.username)}
callback={this.handleDelete}
/>
) : null}
<EuiInMemoryTable
itemId="username"
columns={columns}
selection={selectionConfig}
pagination={pagination}
items={usersToShow}
loading={users.length === 0}
search={search}
sorting={sorting}
rowProps={rowProps}
isSelectable
/>
</EuiPageContentBody>
</EuiPageContent>
</EuiPageBody>
</EuiPage>
);
}
}

View file

@ -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);
}
};
};

View file

@ -1,184 +1,3 @@
<kbn-management-app section="security" omit-breadcrumb-pages="['edit']">
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem">
<!-- Subheader -->
<div class="kuiBar kuiVerticalRhythm">
<div class="kuiBarSection">
<!-- Title -->
<h1 class="kuiTitle">
<span ng-if="editUser.isNewUser">
Add user
</span>
<span ng-if="!editUser.isNewUser">
&ldquo;{{ user.username }}&rdquo; User
</span>
</h1>
</div>
<div class="kuiBarSection">
<!-- Delete button -->
<button
ng-if="!editUser.isNewUser && !user.metadata._reserved"
class="kuiButton kuiButton--danger kuiButton--iconText"
ng-click="deleteUser(user)"
tooltip="Delete User"
data-test-subj="userFormDeleteButton"
>
<span class="kuiButton__inner">
<span class="kuiButton__icon kuiIcon fa-trash"></span>
<span>Delete user</span>
</span>
</button>
<div
ng-if="user.metadata._reserved"
class="kuiBadge kuiBadge--default"
tooltip="Reserved users are built-in and cannot be removed. Only the password can be changed."
>
<span class="kuiIcon fa-lock"></span>
Reserved
</div>
</div>
</div>
<!-- Form -->
<form name="form" novalidate class="kuiVerticalRhythm">
<!-- Username -->
<div class="kuiFormSection">
<label for="username" class="kuiFormLabel">
Username
</label>
<input
type="text"
ng-class="::editUser.isNewUser ? 'kuiTextInput fullWidth' : 'kuiStaticInput'"
ng-disabled="!editUser.isNewUser"
id="username"
name="username"
ng-model="user.username"
required pattern="[a-zA-Z_][a-zA-Z0-9_@\-\$\.]*"
maxlength="30"
ng-disabled="!editUser.isNewUser"
data-test-subj="userFormUserNameInput"
/>
<!-- Errors -->
<div
class="kuiInputNote kuiInputNote--danger"
ng-show="form.username.$error.pattern"
>
Username must begin with a letter or underscore and contain only letters, underscores, and numbers
</div>
<div
class="kuiInputNote kuiInputNote--danger"
ng-show="form.username.$touched && form.username.$error.required"
>
Username is required
</div>
</div>
<!-- New user password -->
<kbn-password-form
ng-if="editUser.isNewUser"
password="user.password"
></kbn-password-form>
<!-- Full name -->
<div class="kuiFormSection" ng-if="!user.metadata._reserved">
<label for="fullname" class="kuiFormLabel">
Full name
</label>
<input
type="text"
class="kuiTextInput fullWidth"
id="fullname"
name="fullname"
ng-model="user.full_name"
required
ng-disabled="editUser.changePasswordMode"
data-test-subj="userFormFullNameInput"
/>
<!-- Errors -->
<div
class="kuiInputNote kuiInputNote--danger"
ng-show="form.fullname.$touched && form.fullname.$error.required"
>
Full name is required
</div>
</div>
<!-- Email-->
<div class="kuiFormSection" ng-if="!user.metadata._reserved">
<label for="email" class="kuiFormLabel">
Email
</label>
<input
type="email"
class="kuiTextInput fullWidth"
id="email"
name="email"
ng-model="user.email"
required
ng-disabled="editUser.changePasswordMode"
data-test-subj="userFormEmailInput"
/>
<!-- Errors -->
<div
class="kuiInputNote kuiInputNote--danger"
ng-show="form.email.$touched && form.email.$error.required"
>
Email is required
</div>
</div>
<!-- Roles-->
<div class="kuiFormSection">
<label class="kuiFormLabel">
Roles
</label>
<ui-select
multiple
ng-model="user.roles"
ng-disabled="user.metadata._reserved"
data-test-subj="userFormRolesDropdown"
>
<ui-select-match placeholder="Start typing for suggestions">
<span data-test-subj="userRole userRole-{{$item}}">{{$item}}</span>
</ui-select-match>
<ui-select-choices repeat="role as role in availableRoles | filter:$select.search">
<div ng-bind-html="role" data-test-subj="addRoleOption addRoleOption-{{role}}" ></div>
</ui-select-choices>
</ui-select>
</div>
</form>
<!-- Form actions -->
<div class="kuiFormSection kuiFormFooter">
<button
class="kuiButton kuiButton--primary"
ng-if="!user.metadata._reserved"
ng-disabled="form.$invalid"
ng-click="saveUser(user)"
data-test-subj="userFormSaveButton"
>
Add user
</button>
<a
class="kuiButton kuiButton--basic"
ng-if="!user.metadata._reserved"
ng-href="{{usersHref}}"
data-test-subj="userFormCancelButton"
>
Cancel
</a>
</div>
<!-- Existing user password -->
<kbn-change-password-form
ng-if="!editUser.isNewUser"
require-current-password="me.username === user.username"
show-kibana-warning="user.metadata._reserved && user.username === 'kibana'"
on-change-password="saveNewPassword(newPassword, currentPassword, onSuccess, onIncorrectPassword)"
></kbn-change-password-form>
</div>
<div id="editUserReactRoot" />
</kbn-management-app>

View file

@ -3,107 +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 { GateKeeperProvider } from 'plugins/xpack_main/services/gate_keeper';
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(
<EditUser
changeUrl={changeUrl}
apiClient={createApiClient(httpClient)}
username={username}
/>,
elem
);
};
routes.when(`${EDIT_USERS_PATH}/:username?`, {
template,
resolve: {
tribeRedirect(Private) {
const gateKeeper = Private(GateKeeperProvider);
gateKeeper.redirectAndNotifyIfTribe();
},
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);
});
},
});

View file

@ -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;
}

View file

@ -1,251 +1,3 @@
<kbn-management-app section="security">
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem">
<div class="kuiInfoPanel kuiInfoPanel--error" ng-if="forbidden">
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
<span class="kuiInfoPanelHeader__title">
You do not have permission to manage users.
</span>
</div>
<div class="kuiInfoPanelBody">
<div class="kuiInfoPanelBody__message">
Please contact your administrator.
</div>
</div>
</div>
<div ng-if="!forbidden">
<!-- ControlledTable -->
<div class="kuiControlledTable">
<!-- ToolBar -->
<div class="kuiToolBar">
<div class="kuiToolBarSearch">
<div class="kuiToolBarSearchBox">
<div class="kuiToolBarSearchBox__icon kuiIcon fa-search"></div>
<input
class="kuiToolBarSearchBox__input"
type="text"
placeholder="Search"
aria-label="Filter"
ng-model="query"
>
</div>
</div>
<div class="kuiToolBarSection">
<!-- Delete users button -->
<button
ng-click="deleteUsers()"
class="kuiButton kuiButton--danger kuiButton--iconText"
ng-if="selectedUsers.length"
>
<span class="kuiButton__inner">
<span class="kuiButton__icon kuiIcon fa-trash"></span>
<span>Delete</span>
<span class="kuiButton__inner">
</button>
<!-- Create user button -->
<a
ng-href="{{editUsersHref}}"
ng-click="newUser()"
class="kuiButton kuiButton--primary kuiButton--iconText"
ng-if="!selectedUsers.length"
data-test-subj="createUserButton"
>
<span class="kuiButton__inner">
<span class="kuiButton__icon kuiIcon fa-plus"></span>
<span>Add user</span>
</span>
</a>
</div>
<div class="kuiToolBarSection">
<!-- We need an empty section for the buttons to be positioned consistently. -->
</div>
</div>
<!-- NoResults -->
<div class="kuiPanel kuiPanel--centered" ng-show="!(users | filter:query).length">
<div class="kuiNoItems">
No users <span ng-show="query">match</span> your search criteria
</div>
</div>
<!-- Table -->
<table class="kuiTable" ng-show="(users | filter:query).length">
<thead>
<tr>
<th scope="col" class="kuiTableHeaderCell kuiTableHeaderCell--checkBox">
<div class="kuiTableHeaderCell__liner">
<input
type="checkbox"
class="kuiCheckBox"
ng-checked="allSelected()"
ng-click="toggleAll()"
aria-label="{{allSelected() ? 'Deselect all rows' : 'Select all rows'}}"
>
</div>
</th>
<th
scope="col"
class="kuiTableHeaderCell"
>
<button
class="kuiTableHeaderCellButton"
ng-class="{'kuiTableHeaderCellButton-isSorted': sort.orderBy == 'full_name'}"
ng-click="toggleSort(sort, 'full_name')"
aria-label="{{sort.reverse ? 'Sort full name ascending' : 'Sort full name descending'}}"
>
<span class="kuiTableHeaderCell__liner">
Full name
<span
aria-hidden="true"
class="kuiTableSortIcon kuiIcon"
ng-class="getSortArrowClass('full_name')"
></span>
</span>
</button>
</th>
<th
scope="col"
class="kuiTableHeaderCell"
>
<button
class="kuiTableHeaderCellButton"
ng-class="{'kuiTableHeaderCellButton-isSorted': sort.orderBy == 'username'}"
ng-click="toggleSort(sort, 'username')"
aria-label="{{sort.reverse ? 'Sort user name ascending' : 'Sort user name descending'}}"
>
<span class="kuiTableHeaderCell__liner">
Username
<span
aria-hidden="true"
class="kuiTableSortIcon kuiIcon"
ng-class="getSortArrowClass('username')"
></span>
</span>
</button>
</th>
<th
scope="col"
class="kuiTableHeaderCell"
>
<button
class="kuiTableHeaderCellButton"
ng-class="{'kuiTableHeaderCellButton-isSorted': sort.orderBy == 'roles'}"
ng-click="toggleSort(sort, 'roles')"
aria-label="{{sort.reverse ? 'Sort roles ascending' : 'Sort roles descending'}}"
>
<span class="kuiTableHeaderCell__liner">
Roles
<span
aria-hidden="true"
class="kuiTableSortIcon kuiIcon"
ng-class="getSortArrowClass('roles')"
></span>
</span>
</button>
</th>
<th scope="col" class="kuiTableHeaderCell">
<span class="kuiTableHeaderCell__liner">
Reserved
<span
class="kuiIcon fa-question-circle"
tooltip="Reserved users are built-in and cannot be removed. Only the password can be changed."
aria-label="Reserved users are built-in and cannot be removed. Only the password can be changed."
></span>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
ng-repeat="user in users | orderBy:'username' | filter:query | orderBy:sort.orderBy:sort.reverse"
data-test-subj="userRow"
class="kuiTableRow"
>
<td class="kuiTableRowCell kuiTableRowCell--checkBox">
<div class="kuiTableRowCell__liner">
<input
type="checkbox"
class="kuiCheckBox"
ng-click="toggle(selectedUsers, user)"
ng-checked="includes(selectedUsers, user)"
ng-disabled="user.metadata._reserved"
>
</div>
</td>
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<a
class="kuiLink"
ng-href="{{getEditUrlHref(user.username)}}"
data-test-subj="userRowFullName"
>
{{ user.full_name }}
</a>
</div>
</td>
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<a
class="kuiLink"
ng-href="{{getEditUrlHref(user.username)}}"
data-test-subj="userRowUserName"
>
{{ user.username }}
</a>
</div>
</td>
<td class="kuiTableRowCell" data-test-subj="userRowRoles">
<div class="kuiTableRowCell__liner">
<span ng-repeat="role in user.roles">
<a
class="kuiLink"
ng-href="{{getEditRoleHref(role)}}"
>
{{ role }}
</a>
<span ng-if="!$last">,</span>
</span>
</div>
</td>
<td class="kuiTableRowCell">
<div class="kuiTableRowCell__liner">
<div
ng-if="user.metadata._reserved"
class="kuiIcon fa-check"
data-test-subj="userRowReserved"
></div>
</div>
</td>
</tr>
</tbody>
</table>
<!-- ToolBarFooter -->
<div class="kuiToolBarFooter">
<div class="kuiToolBarFooterSection">
<div class="kuiToolBarText" ng-hide="selectedUsers.length === 0">
{{ selectedUsers.length }} users selected
</div>
</div>
<div class="kuiToolBarFooterSection">
<!-- We need an empty section for the buttons to be positioned consistently. -->
</div>
</div>
</div>
</div>
</div>
<div id="usersReactRoot" />
</kbn-management-app>

View file

@ -4,95 +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 { GateKeeperProvider } from 'plugins/xpack_main/services/gate_keeper';
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(<Users changeUrl={changeUrl} apiClient={createApiClient(httpClient)} />, elem);
};
routes.when(USERS_PATH, {
template,
resolve: {
tribeRedirect(Private) {
const gateKeeper = Private(GateKeeperProvider);
gateKeeper.redirectAndNotifyIfTribe();
},
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, $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();
};
renderReact(elem, $http, changeUrl);
});
},
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)'
};
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);
}
}
});

View file

@ -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');

View file

@ -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');

View file

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

View file

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