mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Convert account screen to React/EUI (#30977)
* WIP account management redesign * style updates * start implementing change password logic * restyle * remove api key management section * improved change password validation * first round of design edits * cleanup and testing * fix import * fix translations * fix error handling on user management page * consolidate password change logic * fix tests * happy linter, happy life * finish change password test * removes unused translations * fix typo in test * fix change password functional test * Design edits (#19) - Made `fullWidth` - Added a consistent password requirement help text - Use `title` for toast - Change username/email to us `dl` - Don’t use html elements in tests * clear password form on success * copy edits * fix handling of Change Password button * use encodeURIComponent for user supplied data
This commit is contained in:
parent
a6d977e794
commit
031682d7d0
24 changed files with 948 additions and 256 deletions
|
@ -6,6 +6,5 @@
|
|||
|
||||
export const GLOBAL_RESOURCE = '*';
|
||||
export const IGNORED_TYPES = ['space'];
|
||||
export const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];
|
||||
export const APPLICATION_PREFIX = 'kibana-';
|
||||
export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*';
|
||||
|
|
62
x-pack/plugins/security/common/model/user.test.ts
Normal file
62
x-pack/plugins/security/common/model/user.test.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { canUserChangePassword, getUserDisplayName, User } from './user';
|
||||
|
||||
describe('#getUserDisplayName', () => {
|
||||
it(`uses the full name when available`, () => {
|
||||
expect(
|
||||
getUserDisplayName({
|
||||
full_name: 'my full name',
|
||||
username: 'foo',
|
||||
} as User)
|
||||
).toEqual('my full name');
|
||||
});
|
||||
|
||||
it(`uses the username when full name is not available`, () => {
|
||||
expect(
|
||||
getUserDisplayName({
|
||||
username: 'foo',
|
||||
} as User)
|
||||
).toEqual('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#canUserChangePassword', () => {
|
||||
['reserved', 'native'].forEach(realm => {
|
||||
it(`returns true for users in the ${realm} realm`, () => {
|
||||
expect(
|
||||
canUserChangePassword({
|
||||
username: 'foo',
|
||||
authentication_realm: {
|
||||
name: 'the realm name',
|
||||
type: realm,
|
||||
},
|
||||
} as User)
|
||||
).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it(`returns true when no realm is provided`, () => {
|
||||
expect(
|
||||
canUserChangePassword({
|
||||
username: 'foo',
|
||||
} as User)
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
it(`returns false for all other realms`, () => {
|
||||
expect(
|
||||
canUserChangePassword({
|
||||
username: 'foo',
|
||||
authentication_realm: {
|
||||
name: 'the realm name',
|
||||
type: 'does not matter',
|
||||
},
|
||||
} as User)
|
||||
).toEqual(false);
|
||||
});
|
||||
});
|
37
x-pack/plugins/security/common/model/user.ts
Normal file
37
x-pack/plugins/security/common/model/user.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 interface User {
|
||||
username: string;
|
||||
email: string;
|
||||
full_name: string;
|
||||
roles: string[];
|
||||
enabled: boolean;
|
||||
authentication_realm?: {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
lookup_realm?: {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
||||
|
||||
const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native'];
|
||||
|
||||
export function getUserDisplayName(user: User): string {
|
||||
return user.full_name || user.username;
|
||||
}
|
||||
|
||||
export function canUserChangePassword(user: User): boolean {
|
||||
const { authentication_realm: authenticationRealm } = user;
|
||||
|
||||
if (!authenticationRealm) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(authenticationRealm.type);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
jest.mock('../../../lib/api', () => {
|
||||
return {
|
||||
UserAPIClient: {
|
||||
changePassword: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
import { EuiFieldText } from '@elastic/eui';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { User } from '../../../../common/model/user';
|
||||
import { UserAPIClient } from '../../../lib/api';
|
||||
import { ChangePasswordForm } from './change_password_form';
|
||||
|
||||
function getCurrentPasswordField(wrapper: ReactWrapper<any>) {
|
||||
return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]');
|
||||
}
|
||||
|
||||
function getNewPasswordField(wrapper: ReactWrapper<any>) {
|
||||
return wrapper.find(EuiFieldText).filter('[data-test-subj="newPassword"]');
|
||||
}
|
||||
|
||||
function getConfirmPasswordField(wrapper: ReactWrapper<any>) {
|
||||
return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]');
|
||||
}
|
||||
|
||||
describe('<ChangePasswordForm>', () => {
|
||||
describe('for the current user', () => {
|
||||
it('shows fields for current and new passwords', () => {
|
||||
const user: User = {
|
||||
username: 'user',
|
||||
full_name: 'john smith',
|
||||
email: 'john@smith.com',
|
||||
enabled: true,
|
||||
roles: [],
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ChangePasswordForm user={user} isUserChangingOwnPassword={true} />
|
||||
);
|
||||
|
||||
expect(getCurrentPasswordField(wrapper)).toHaveLength(1);
|
||||
expect(getNewPasswordField(wrapper)).toHaveLength(1);
|
||||
expect(getConfirmPasswordField(wrapper)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('allows a password to be changed', () => {
|
||||
const user: User = {
|
||||
username: 'user',
|
||||
full_name: 'john smith',
|
||||
email: 'john@smith.com',
|
||||
enabled: true,
|
||||
roles: [],
|
||||
};
|
||||
|
||||
const callback = jest.fn();
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ChangePasswordForm
|
||||
user={user}
|
||||
isUserChangingOwnPassword={true}
|
||||
onChangePassword={callback}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentPassword = getCurrentPasswordField(wrapper);
|
||||
currentPassword.props().onChange!({ target: { value: 'myCurrentPassword' } } as any);
|
||||
|
||||
const newPassword = getNewPasswordField(wrapper);
|
||||
newPassword.props().onChange!({ target: { value: 'myNewPassword' } } as any);
|
||||
|
||||
const confirmPassword = getConfirmPasswordField(wrapper);
|
||||
confirmPassword.props().onChange!({ target: { value: 'myNewPassword' } } as any);
|
||||
|
||||
wrapper.find('button[data-test-subj="changePasswordButton"]').simulate('click');
|
||||
|
||||
expect(UserAPIClient.changePassword).toHaveBeenCalledTimes(1);
|
||||
expect(UserAPIClient.changePassword).toHaveBeenCalledWith(
|
||||
'user',
|
||||
'myNewPassword',
|
||||
'myCurrentPassword'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for another user', () => {
|
||||
it('shows fields for new password only', () => {
|
||||
const user: User = {
|
||||
username: 'user',
|
||||
full_name: 'john smith',
|
||||
email: 'john@smith.com',
|
||||
enabled: true,
|
||||
roles: [],
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<ChangePasswordForm user={user} isUserChangingOwnPassword={false} />
|
||||
);
|
||||
|
||||
expect(getCurrentPasswordField(wrapper)).toHaveLength(0);
|
||||
expect(getNewPasswordField(wrapper)).toHaveLength(1);
|
||||
expect(getConfirmPasswordField(wrapper)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,327 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
// @ts-ignore
|
||||
EuiButtonEmpty,
|
||||
// @ts-ignore
|
||||
EuiDescribedFormGroup,
|
||||
EuiFieldText,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { ChangeEvent, Component } from 'react';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { User } from '../../../../common/model/user';
|
||||
import { UserAPIClient } from '../../../lib/api';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
isUserChangingOwnPassword: boolean;
|
||||
onChangePassword?: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
shouldValidate: boolean;
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
currentPasswordError: boolean;
|
||||
changeInProgress: boolean;
|
||||
}
|
||||
|
||||
function getInitialState(): State {
|
||||
return {
|
||||
shouldValidate: false,
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
currentPasswordError: false,
|
||||
changeInProgress: false,
|
||||
};
|
||||
}
|
||||
|
||||
export class ChangePasswordForm extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = getInitialState();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return this.getForm();
|
||||
}
|
||||
|
||||
private getForm = () => {
|
||||
return (
|
||||
<EuiForm>
|
||||
{this.props.isUserChangingOwnPassword && (
|
||||
<EuiFormRow
|
||||
{...this.validateCurrentPassword()}
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.currentPasswordLabel"
|
||||
defaultMessage="Current password"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="currentPassword"
|
||||
type="password"
|
||||
value={this.state.currentPassword}
|
||||
onChange={this.onCurrentPasswordChange}
|
||||
disabled={this.state.changeInProgress}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
<EuiFormRow
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.passwordRequirements"
|
||||
defaultMessage="Use at least 6 characters."
|
||||
/>
|
||||
}
|
||||
{...this.validateNewPassword()}
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.newPasswordLabel"
|
||||
defaultMessage="New password"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="newPassword"
|
||||
type="password"
|
||||
value={this.state.newPassword}
|
||||
onChange={this.onNewPasswordChange}
|
||||
disabled={this.state.changeInProgress}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
{...this.validateConfirmPassword()}
|
||||
fullWidth
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.confirmPasswordLabel"
|
||||
defaultMessage="Confirm new password"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="confirmNewPassword"
|
||||
type="password"
|
||||
value={this.state.confirmPassword}
|
||||
onChange={this.onConfirmPasswordChange}
|
||||
disabled={this.state.changeInProgress}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={this.onChangePasswordClick}
|
||||
fill
|
||||
isLoading={this.state.changeInProgress}
|
||||
data-test-subj="changePasswordButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.saveChangesButtonLabel"
|
||||
defaultMessage="Change password"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={this.onCancelClick} isDisabled={this.state.changeInProgress}>
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.cancelButtonLabel"
|
||||
defaultMessage="Reset"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</EuiForm>
|
||||
);
|
||||
};
|
||||
|
||||
private onCurrentPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ currentPassword: e.target.value, currentPasswordError: false });
|
||||
};
|
||||
|
||||
private onNewPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ newPassword: e.target.value });
|
||||
};
|
||||
|
||||
private onConfirmPasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ confirmPassword: e.target.value });
|
||||
};
|
||||
|
||||
private onCancelClick = () => {
|
||||
this.setState(getInitialState());
|
||||
};
|
||||
|
||||
private onChangePasswordClick = async () => {
|
||||
this.setState({ shouldValidate: true, currentPasswordError: false }, () => {
|
||||
const { isInvalid } = this.validateForm();
|
||||
if (isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ changeInProgress: true }, () => this.performPasswordChange());
|
||||
});
|
||||
};
|
||||
|
||||
private validateCurrentPassword = (shouldValidate = this.state.shouldValidate) => {
|
||||
if (!shouldValidate || !this.props.isUserChangingOwnPassword) {
|
||||
return {
|
||||
isInvalid: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.state.currentPasswordError) {
|
||||
return {
|
||||
isInvalid: true,
|
||||
error: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordForm.invalidPassword"
|
||||
defaultMessage="Current password is incorrect."
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.state.currentPassword) {
|
||||
return {
|
||||
isInvalid: true,
|
||||
error: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.currentPasswordRequired"
|
||||
defaultMessage="Current password is required."
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isInvalid: false,
|
||||
};
|
||||
};
|
||||
|
||||
private validateNewPassword = (shouldValidate = this.state.shouldValidate) => {
|
||||
const { newPassword } = this.state;
|
||||
const minPasswordLength = 6;
|
||||
if (shouldValidate && newPassword.length < minPasswordLength) {
|
||||
return {
|
||||
isInvalid: true,
|
||||
error: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.passwordLengthDescription"
|
||||
defaultMessage="Password is too short."
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isInvalid: false,
|
||||
};
|
||||
};
|
||||
|
||||
private validateConfirmPassword = (shouldValidate = this.state.shouldValidate) => {
|
||||
const { newPassword, confirmPassword } = this.state;
|
||||
if (shouldValidate && newPassword !== confirmPassword) {
|
||||
return {
|
||||
isInvalid: true,
|
||||
error: (
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.passwordsDoNotMatch"
|
||||
defaultMessage="Passwords do not match."
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isInvalid: false,
|
||||
};
|
||||
};
|
||||
|
||||
private validateForm = () => {
|
||||
const validation = [
|
||||
this.validateCurrentPassword(true),
|
||||
this.validateNewPassword(true),
|
||||
this.validateConfirmPassword(true),
|
||||
];
|
||||
|
||||
const firstFailure = validation.find(result => result.isInvalid);
|
||||
if (firstFailure) {
|
||||
return firstFailure;
|
||||
}
|
||||
|
||||
return {
|
||||
isInvalid: false,
|
||||
};
|
||||
};
|
||||
|
||||
private performPasswordChange = async () => {
|
||||
try {
|
||||
await UserAPIClient.changePassword(
|
||||
this.props.user.username,
|
||||
this.state.newPassword,
|
||||
this.state.currentPassword
|
||||
);
|
||||
this.handleChangePasswordSuccess();
|
||||
} catch (e) {
|
||||
this.handleChangePasswordFailure(e);
|
||||
} finally {
|
||||
this.setState({
|
||||
changeInProgress: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleChangePasswordSuccess = () => {
|
||||
toastNotifications.addSuccess({
|
||||
title: i18n.translate('xpack.security.account.changePasswordSuccess', {
|
||||
defaultMessage: 'Your password has been changed.',
|
||||
}),
|
||||
'data-test-subj': 'passwordUpdateSuccess',
|
||||
});
|
||||
|
||||
this.setState({
|
||||
currentPasswordError: false,
|
||||
shouldValidate: false,
|
||||
newPassword: '',
|
||||
currentPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
if (this.props.onChangePassword) {
|
||||
this.props.onChangePassword();
|
||||
}
|
||||
};
|
||||
|
||||
private handleChangePasswordFailure = (error: Record<string, any>) => {
|
||||
if (error.body && error.body.statusCode === 401) {
|
||||
this.setState({ currentPasswordError: true });
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.security.management.users.editUser.settingPasswordErrorMessage', {
|
||||
defaultMessage: 'Error setting password: {message}',
|
||||
values: { message: _.get(error, 'body.message') },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { ChangePasswordForm } from './change_password_form';
|
|
@ -8,14 +8,15 @@ import React, { Component, Fragment } from 'react';
|
|||
import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import { UserAPIClient } from '../../../lib/api';
|
||||
|
||||
class ConfirmDeleteUI extends Component {
|
||||
deleteUsers = () => {
|
||||
const { usersToDelete, apiClient, callback } = this.props;
|
||||
const { usersToDelete, callback } = this.props;
|
||||
const errors = [];
|
||||
usersToDelete.forEach(async username => {
|
||||
try {
|
||||
await apiClient.deleteUser(username);
|
||||
await UserAPIClient.deleteUser(username);
|
||||
toastNotifications.addSuccess(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage',
|
||||
|
|
|
@ -31,6 +31,8 @@ import { toastNotifications } from 'ui/notify';
|
|||
import { USERS_PATH } from '../../../views/management/management_urls';
|
||||
import { ConfirmDelete } from './confirm_delete';
|
||||
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
|
||||
import { UserAPIClient } from '../../../lib/api';
|
||||
import { ChangePasswordForm } from '../change_password_form';
|
||||
|
||||
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_@\-\$\.]*/;
|
||||
|
@ -55,36 +57,35 @@ class EditUserUI extends Component {
|
|||
};
|
||||
}
|
||||
async componentDidMount() {
|
||||
const { apiClient, username } = this.props;
|
||||
const { username } = this.props;
|
||||
let { user, currentUser } = this.state;
|
||||
if (username) {
|
||||
try {
|
||||
user = await apiClient.getUser(username);
|
||||
currentUser = await apiClient.getCurrentUser();
|
||||
user = await UserAPIClient.getUser(username);
|
||||
currentUser = await UserAPIClient.getCurrentUser();
|
||||
} catch (err) {
|
||||
toastNotifications.addDanger({
|
||||
title: this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.errorLoadingUserTitle',
|
||||
defaultMessage: 'Error loading user'
|
||||
}),
|
||||
text: get(err, 'data.message') || err.message,
|
||||
text: get(err, 'body.message') || err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let roles;
|
||||
let roles = [];
|
||||
try {
|
||||
roles = await apiClient.getRoles();
|
||||
roles = await UserAPIClient.getRoles();
|
||||
} catch (err) {
|
||||
toastNotifications.addDanger({
|
||||
title: this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.errorLoadingRolesTitle',
|
||||
defaultMessage: 'Error loading roles'
|
||||
}),
|
||||
text: get(err, 'data.message') || err.message,
|
||||
text: get(err, 'body.message') || err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -153,10 +154,9 @@ class EditUserUI extends Component {
|
|||
}
|
||||
};
|
||||
changePassword = async () => {
|
||||
const { apiClient } = this.props;
|
||||
const { user, password, currentPassword } = this.state;
|
||||
try {
|
||||
await apiClient.changePassword(user.username, password, currentPassword);
|
||||
await UserAPIClient.changePassword(user.username, password, currentPassword);
|
||||
toastNotifications.addSuccess(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.passwordSuccessfullyChangedNotificationMessage',
|
||||
|
@ -164,21 +164,21 @@ class EditUserUI extends Component {
|
|||
})
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.status === 401) {
|
||||
if (e.body.statusCode === 401) {
|
||||
return this.setState({ currentPasswordError: true });
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.settingPasswordErrorMessage',
|
||||
defaultMessage: 'Error setting password: {message}'
|
||||
}, { message: e.data.message })
|
||||
}, { message: get(e, 'body.message', 'Unknown error') })
|
||||
);
|
||||
}
|
||||
}
|
||||
this.clearPasswordForm();
|
||||
};
|
||||
saveUser = async () => {
|
||||
const { apiClient, changeUrl } = this.props;
|
||||
const { changeUrl } = this.props;
|
||||
const { user, password, selectedRoles } = this.state;
|
||||
const userToSave = { ...user };
|
||||
userToSave.roles = selectedRoles.map(selectedRole => {
|
||||
|
@ -188,7 +188,7 @@ class EditUserUI extends Component {
|
|||
userToSave.password = password;
|
||||
}
|
||||
try {
|
||||
await apiClient.saveUser(userToSave);
|
||||
await UserAPIClient.saveUser(userToSave);
|
||||
toastNotifications.addSuccess(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage',
|
||||
|
@ -201,7 +201,7 @@ class EditUserUI extends Component {
|
|||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.savingUserErrorMessage',
|
||||
defaultMessage: 'Error saving user: {message}'
|
||||
}, { message: e.data.message })
|
||||
}, { message: get(e, 'body.message', 'Unknown error') })
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -213,32 +213,11 @@ class EditUserUI extends Component {
|
|||
});
|
||||
};
|
||||
passwordFields = () => {
|
||||
const { user, currentUser } = this.state;
|
||||
const userIsLoggedInUser = user.username && user.username === currentUser.username;
|
||||
return (
|
||||
<Fragment>
|
||||
{userIsLoggedInUser ? (
|
||||
<EuiFormRow
|
||||
label={this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.currentPasswordFormRowLabel',
|
||||
defaultMessage: '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 ? this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.newPasswordFormRowLabel',
|
||||
defaultMessage: 'New password'
|
||||
}) : this.props.intl.formatMessage({
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.editUser.passwordFormRowLabel',
|
||||
defaultMessage: 'Password'
|
||||
})
|
||||
|
@ -276,18 +255,19 @@ class EditUserUI extends Component {
|
|||
changePasswordForm = () => {
|
||||
const {
|
||||
showChangePasswordForm,
|
||||
password,
|
||||
confirmPassword,
|
||||
user: { username },
|
||||
user,
|
||||
currentUser,
|
||||
} = this.state;
|
||||
|
||||
const userIsLoggedInUser = user.username && user.username === currentUser.username;
|
||||
|
||||
if (!showChangePasswordForm) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiHorizontalRule />
|
||||
{this.passwordFields()}
|
||||
{username === 'kibana' ? (
|
||||
{user.username === 'kibana' ? (
|
||||
<Fragment>
|
||||
<EuiCallOut
|
||||
title={this.props.intl.formatMessage({
|
||||
|
@ -309,38 +289,11 @@ class EditUserUI extends Component {
|
|||
<EuiSpacer />
|
||||
</Fragment>
|
||||
) : null}
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
fill
|
||||
disabled={
|
||||
!password || !confirmPassword || this.passwordError() || this.confirmPasswordError()
|
||||
}
|
||||
onClick={() => {
|
||||
this.changePassword(password);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.users.editUser.savePasswordButtonLabel"
|
||||
defaultMessage="Save password"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
onClick={() => {
|
||||
this.clearPasswordForm();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.security.management.users.editUser.savePasswordCancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<ChangePasswordForm
|
||||
user={this.state.user}
|
||||
isUserChangingOwnPassword={userIsLoggedInUser}
|
||||
onChangePassword={this.toggleChangePasswordForm}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -365,7 +318,7 @@ class EditUserUI extends Component {
|
|||
this.setState({ showDeleteConfirmation: false });
|
||||
};
|
||||
render() {
|
||||
const { changeUrl, apiClient, intl } = this.props;
|
||||
const { changeUrl, intl } = this.props;
|
||||
const {
|
||||
user,
|
||||
roles,
|
||||
|
@ -427,7 +380,6 @@ class EditUserUI extends Component {
|
|||
{showDeleteConfirmation ? (
|
||||
<ConfirmDelete
|
||||
onCancel={this.onCancelDelete}
|
||||
apiClient={apiClient}
|
||||
usersToDelete={[user.username]}
|
||||
callback={this.handleDelete}
|
||||
/>
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { toastNotifications } from 'ui/notify';
|
||||
import { ConfirmDelete } from './confirm_delete';
|
||||
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
|
||||
import { UserAPIClient } from '../../../lib/api';
|
||||
|
||||
class UsersUI extends Component {
|
||||
constructor(props) {
|
||||
|
@ -44,19 +45,18 @@ class UsersUI extends Component {
|
|||
});
|
||||
};
|
||||
async loadUsers() {
|
||||
const { apiClient } = this.props;
|
||||
try {
|
||||
const users = await apiClient.getUsers();
|
||||
const users = await UserAPIClient.getUsers();
|
||||
this.setState({ users });
|
||||
} catch (e) {
|
||||
if (e.status === 403) {
|
||||
if (e.body.statusCode === 403) {
|
||||
this.setState({ permissionDenied: true });
|
||||
} else {
|
||||
toastNotifications.addDanger(
|
||||
this.props.intl.formatMessage({
|
||||
id: 'xpack.security.management.users.fetchingUsersErrorMessage',
|
||||
defaultMessage: 'Error fetching users: {message}'
|
||||
}, { message: e.data.message })
|
||||
}, { message: e.body.message })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class UsersUI extends Component {
|
|||
}
|
||||
render() {
|
||||
const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state;
|
||||
const { apiClient, intl } = this.props;
|
||||
const { intl } = this.props;
|
||||
if (permissionDenied) {
|
||||
return (
|
||||
<div className="secUsersListingPage">
|
||||
|
@ -251,7 +251,6 @@ class UsersUI extends Component {
|
|||
{showDeleteConfirmation ? (
|
||||
<ConfirmDelete
|
||||
onCancel={this.onCancelDelete}
|
||||
apiClient={apiClient}
|
||||
usersToDelete={selection.map((user) => user.username)}
|
||||
callback={this.handleDelete}
|
||||
/>
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* 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/role');
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
};
|
60
x-pack/plugins/security/public/lib/api.ts
Normal file
60
x-pack/plugins/security/public/lib/api.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { kfetch } from 'ui/kfetch';
|
||||
import { Role } from '../../common/model/role';
|
||||
import { User } from '../../common/model/user';
|
||||
|
||||
const usersUrl = '/api/security/v1/users';
|
||||
const rolesUrl = '/api/security/role';
|
||||
|
||||
export class UserAPIClient {
|
||||
public static async getCurrentUser(): Promise<User> {
|
||||
return await kfetch({ pathname: `/api/security/v1/me` });
|
||||
}
|
||||
|
||||
public static async getUsers(): Promise<User[]> {
|
||||
return await kfetch({ pathname: usersUrl });
|
||||
}
|
||||
|
||||
public static async getUser(username: string): Promise<User> {
|
||||
const url = `${usersUrl}/${encodeURIComponent(username)}`;
|
||||
return await kfetch({ pathname: url });
|
||||
}
|
||||
|
||||
public static async deleteUser(username: string) {
|
||||
const url = `${usersUrl}/${encodeURIComponent(username)}`;
|
||||
await kfetch({ pathname: url, method: 'DELETE' }, {});
|
||||
}
|
||||
|
||||
public static async saveUser(user: User) {
|
||||
const url = `${usersUrl}/${encodeURIComponent(user.username)}`;
|
||||
await kfetch({ pathname: url, body: JSON.stringify(user), method: 'POST' });
|
||||
}
|
||||
|
||||
public static async getRoles(): Promise<Role[]> {
|
||||
return await kfetch({ pathname: rolesUrl });
|
||||
}
|
||||
|
||||
public static async getRole(name: string): Promise<Role> {
|
||||
const url = `${rolesUrl}/${encodeURIComponent(name)}`;
|
||||
return await kfetch({ pathname: url });
|
||||
}
|
||||
|
||||
public static async changePassword(username: string, password: string, currentPassword: string) {
|
||||
const data: Record<string, string> = {
|
||||
newPassword: password,
|
||||
};
|
||||
if (currentPassword) {
|
||||
data.password = currentPassword;
|
||||
}
|
||||
await kfetch({
|
||||
pathname: `${usersUrl}/${encodeURIComponent(username)}/password`,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,54 +1 @@
|
|||
<div class="kuiViewContent kuiViewContent--constrainedWidth kuiViewContentItem">
|
||||
<!-- Subheader -->
|
||||
<div class="kuiBar kuiVerticalRhythm">
|
||||
<!-- Title -->
|
||||
<div class="kuiBarSection">
|
||||
<h1
|
||||
class="kuiTitle"
|
||||
i18n-id="xpack.security.account.accountSettingsTitle"
|
||||
i18n-default-message="Account Settings"
|
||||
></h1>
|
||||
</div>
|
||||
|
||||
<!-- Empty section to push content left -->
|
||||
<div class="kuiBarSection"></div>
|
||||
</div>
|
||||
|
||||
<!-- Username -->
|
||||
<div class="kuiFormSection kuiVerticalRhythm">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
i18n-id="xpack.security.account.usernameLabel"
|
||||
i18n-default-message="Username"
|
||||
></label>
|
||||
<div class="euiText">
|
||||
<p ng-bind="::user.username" data-test-subj=usernameField></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="kuiFormSection">
|
||||
<label
|
||||
class="kuiFormLabel"
|
||||
i18n-id="xpack.security.account.emailLabel"
|
||||
i18n-default-message="Email"
|
||||
></label>
|
||||
<div class="euiText">
|
||||
<p ng-bind="::accountController.getEmail()" data-test-subj=emailIdField></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Change password -->
|
||||
<div class="kuiFormSection" ng-if="!showChangePassword">
|
||||
<label class="kuiFormLabel" i18n-id="xpack.security.account.passwordLabel" i18n-default-message="Password"></label>
|
||||
<div class="euiText">
|
||||
<p i18n-id="xpack.security.account.changePasswordNotSupportedText" i18n-default-message="You cannot change the password for this account."></p>
|
||||
</div>
|
||||
</div>
|
||||
<kbn-change-password-form
|
||||
ng-if="showChangePassword"
|
||||
require-current-password="true"
|
||||
show-kibana-warning="user.metadata._reserved && user.username === 'kibana'"
|
||||
on-change-password="saveNewPassword(newPassword, currentPassword, onSuccess, onIncorrectPassword)"
|
||||
></kbn-change-password-form>
|
||||
</div>
|
||||
<div id="userProfileReactRoot" />
|
||||
|
|
|
@ -4,21 +4,32 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import _ from 'lodash';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import routes from 'ui/routes';
|
||||
import template from './account.html';
|
||||
import '../management/change_password_form/change_password_form';
|
||||
import '../../services/shield_user';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE } from '../../../common/constants';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { AccountManagementPage } from './components';
|
||||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
const renderReact = (elem, user) => {
|
||||
render(
|
||||
<I18nContext>
|
||||
<AccountManagementPage
|
||||
user={user}
|
||||
/>
|
||||
</I18nContext>,
|
||||
elem
|
||||
);
|
||||
};
|
||||
|
||||
routes.when('/account', {
|
||||
template,
|
||||
k7Breadcrumbs: () => [
|
||||
{
|
||||
text: i18n.translate('xpack.security.account.breadcrumb', {
|
||||
defaultMessage: 'Account',
|
||||
defaultMessage: 'Account Management',
|
||||
})
|
||||
}
|
||||
],
|
||||
|
@ -28,42 +39,16 @@ routes.when('/account', {
|
|||
}
|
||||
},
|
||||
controllerAs: 'accountController',
|
||||
controller($scope, $route, Notifier, config, i18n) {
|
||||
$scope.user = $route.current.locals.user;
|
||||
|
||||
const notifier = new Notifier();
|
||||
|
||||
const { authentication_realm: authenticationRealm } = $scope.user;
|
||||
$scope.showChangePassword = REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE.includes(authenticationRealm.type);
|
||||
|
||||
$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) {
|
||||
$scope.$on('$destroy', () => {
|
||||
const elem = document.getElementById('userProfileReactRoot');
|
||||
if (elem) {
|
||||
unmountComponentAtNode(elem);
|
||||
}
|
||||
|
||||
$scope.user.$changePassword()
|
||||
.then(() => toastNotifications.addSuccess({
|
||||
title: i18n('xpack.security.account.updatedPasswordTitle', {
|
||||
defaultMessage: 'Updated password'
|
||||
}),
|
||||
'data-test-subj': 'passwordUpdateSuccess',
|
||||
}))
|
||||
.then(onSuccess)
|
||||
.catch(error => {
|
||||
if (error.status === 401) {
|
||||
onIncorrectPassword();
|
||||
}
|
||||
else notifier.error(_.get(error, 'data.message'));
|
||||
});
|
||||
};
|
||||
|
||||
this.getEmail = () => {
|
||||
if ($scope.user.email) return $scope.user.email;
|
||||
return i18n('xpack.security.account.noEmailMessage', {
|
||||
defaultMessage: '(No email)'
|
||||
});
|
||||
};
|
||||
});
|
||||
$scope.$$postDigest(() => {
|
||||
const elem = document.getElementById('userProfileReactRoot');
|
||||
renderReact(elem, $route.current.locals.user);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { User } from '../../../../common/model/user';
|
||||
import { AccountManagementPage } from './account_management_page';
|
||||
|
||||
interface Options {
|
||||
withFullName?: boolean;
|
||||
withEmail?: boolean;
|
||||
realm?: string;
|
||||
}
|
||||
const createUser = ({ withFullName = true, withEmail = true, realm = 'native' }: Options = {}) => {
|
||||
return {
|
||||
full_name: withFullName ? 'Casey Smith' : '',
|
||||
username: 'csmith',
|
||||
email: withEmail ? 'csmith@domain.com' : '',
|
||||
enabled: true,
|
||||
roles: [],
|
||||
authentication_realm: {
|
||||
type: realm,
|
||||
name: realm,
|
||||
},
|
||||
lookup_realm: {
|
||||
type: realm,
|
||||
name: realm,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('<AccountManagementPage>', () => {
|
||||
it(`displays users full name, username, and email address`, () => {
|
||||
const user: User = createUser();
|
||||
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
|
||||
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(
|
||||
user.full_name
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="username"]').text()).toEqual(user.username);
|
||||
expect(wrapper.find('[data-test-subj="email"]').text()).toEqual(user.email);
|
||||
});
|
||||
|
||||
it(`displays username when full_name is not provided`, () => {
|
||||
const user: User = createUser({ withFullName: false });
|
||||
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
|
||||
expect(wrapper.find('EuiText[data-test-subj="userDisplayName"]').text()).toEqual(user.username);
|
||||
});
|
||||
|
||||
it(`displays a placeholder when no email address is provided`, () => {
|
||||
const user: User = createUser({ withEmail: false });
|
||||
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
|
||||
expect(wrapper.find('[data-test-subj="email"]').text()).toEqual('no email address');
|
||||
});
|
||||
|
||||
it(`displays change password form for users in the native realm`, () => {
|
||||
const user: User = createUser();
|
||||
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
|
||||
expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(1);
|
||||
expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it(`does not display change password form for users in the saml realm`, () => {
|
||||
const user: User = createUser({ realm: 'saml' });
|
||||
const wrapper = mountWithIntl(<AccountManagementPage user={user} />);
|
||||
expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(0);
|
||||
expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 {
|
||||
// @ts-ignore
|
||||
EuiDescribedFormGroup,
|
||||
EuiPage,
|
||||
EuiPageBody,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import React, { Component } from 'react';
|
||||
import { getUserDisplayName, User } from '../../../../common/model/user';
|
||||
import { ChangePassword } from './change_password';
|
||||
import { PersonalInfo } from './personal_info';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export class AccountManagementPage extends Component<Props, {}> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<EuiPage>
|
||||
<EuiPageBody restrictWidth>
|
||||
<EuiPanel>
|
||||
<EuiText data-test-subj={'userDisplayName'}>
|
||||
<h1>{getUserDisplayName(this.props.user)}</h1>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="xl" />
|
||||
|
||||
<PersonalInfo user={this.props.user} />
|
||||
|
||||
<ChangePassword user={this.props.user} />
|
||||
</EuiPanel>
|
||||
</EuiPageBody>
|
||||
</EuiPage>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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 {
|
||||
// @ts-ignore
|
||||
EuiButtonEmpty,
|
||||
// @ts-ignore
|
||||
EuiDescribedFormGroup,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Component } from 'react';
|
||||
import { canUserChangePassword, User } from '../../../../../common/model/user';
|
||||
import { ChangePasswordForm } from '../../../..//components/management/change_password_form';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export class ChangePassword extends Component<Props, {}> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const canChangePassword = canUserChangePassword(this.props.user);
|
||||
|
||||
const changePasswordTitle = (
|
||||
<FormattedMessage id="xpack.security.account.changePasswordTitle" defaultMessage="Password" />
|
||||
);
|
||||
|
||||
if (canChangePassword) {
|
||||
return this.getChangePasswordForm(changePasswordTitle);
|
||||
}
|
||||
return this.getChangePasswordUnavailable(changePasswordTitle);
|
||||
}
|
||||
|
||||
private getChangePasswordForm = (changePasswordTitle: React.ReactElement<any>) => {
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={<h3>{changePasswordTitle}</h3>}
|
||||
description={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordDescription"
|
||||
defaultMessage="Change the password for your account."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<ChangePasswordForm user={this.props.user} isUserChangingOwnPassword={true} />
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
private getChangePasswordUnavailable(changePasswordTitle: React.ReactElement<any>) {
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={<h3>{changePasswordTitle}</h3>}
|
||||
description={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.changePasswordNotSupportedText"
|
||||
defaultMessage="You cannot change the password for this account."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<div />
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { ChangePassword } from './change_password';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { AccountManagementPage } from './account_management_page';
|
|
@ -0,0 +1,7 @@
|
|||
/*
|
||||
* 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 { PersonalInfo } from './personal_info';
|
|
@ -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 {
|
||||
// @ts-ignore
|
||||
EuiDescribedFormGroup,
|
||||
EuiFormRow,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { User } from '../../../../../common/model/user';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const PersonalInfo = (props: Props) => {
|
||||
return (
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.usernameGroupTitle"
|
||||
defaultMessage="Username and email"
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.usernameGroupDescription"
|
||||
defaultMessage="You can't change this information."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiText size="s">
|
||||
<dl>
|
||||
<dt title="username" data-test-subj="username">
|
||||
{props.user.username}
|
||||
</dt>
|
||||
<dd title="email" data-test-subj="email">
|
||||
{props.user.email || (
|
||||
<FormattedMessage
|
||||
id="xpack.security.account.noEmailMessage"
|
||||
defaultMessage="no email address"
|
||||
/>
|
||||
)}
|
||||
</dd>
|
||||
</dl>
|
||||
</EuiText>
|
||||
</EuiFormRow>
|
||||
</EuiDescribedFormGroup>
|
||||
);
|
||||
};
|
|
@ -13,7 +13,6 @@ 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';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { getEditUserBreadcrumbs, getCreateUserBreadcrumbs } from './breadcrumbs';
|
||||
|
||||
|
@ -22,7 +21,6 @@ const renderReact = (elem, httpClient, changeUrl, username) => {
|
|||
<I18nContext>
|
||||
<EditUser
|
||||
changeUrl={changeUrl}
|
||||
apiClient={createApiClient(httpClient)}
|
||||
username={username}
|
||||
/>
|
||||
</I18nContext>,
|
||||
|
|
|
@ -11,7 +11,6 @@ import template from 'plugins/security/views/management/users.html';
|
|||
import 'plugins/security/services/shield_user';
|
||||
import { SECURITY_PATH, USERS_PATH } from './management_urls';
|
||||
import { Users } from '../../components/management/users';
|
||||
import { createApiClient } from '../../lib/api';
|
||||
import { I18nContext } from 'ui/i18n';
|
||||
import { getUsersBreadcrumbs } from './breadcrumbs';
|
||||
|
||||
|
@ -19,8 +18,8 @@ routes.when(SECURITY_PATH, {
|
|||
redirectTo: USERS_PATH,
|
||||
});
|
||||
|
||||
const renderReact = (elem, httpClient, changeUrl) => {
|
||||
render(<I18nContext><Users changeUrl={changeUrl} apiClient={createApiClient(httpClient)} /></I18nContext>, elem);
|
||||
const renderReact = (elem, changeUrl) => {
|
||||
render(<I18nContext><Users changeUrl={changeUrl} /></I18nContext>, elem);
|
||||
};
|
||||
|
||||
routes.when(USERS_PATH, {
|
||||
|
|
|
@ -7403,13 +7403,8 @@
|
|||
"xpack.searchProfiler.trialLicenseTitle": "试用",
|
||||
"xpack.searchProfiler.unavailableLicenseInformationMessage": "Search Profiler 不可用 - 许可信息当前不可用。",
|
||||
"xpack.searchProfiler.upgradeLicenseMessage": "Search Profiler 不可用于当前的{licenseInfo}许可。请升级您的许可。",
|
||||
"xpack.security.account.accountSettingsTitle": "帐户设置",
|
||||
"xpack.security.account.changePasswordNotSupportedText": "不能更改此帐户的密码。",
|
||||
"xpack.security.account.emailLabel": "电子邮件",
|
||||
"xpack.security.account.noEmailMessage": "(无电子邮件)",
|
||||
"xpack.security.account.passwordLabel": "密码",
|
||||
"xpack.security.account.updatedPasswordTitle": "更新的密码",
|
||||
"xpack.security.account.usernameLabel": "用户名",
|
||||
"xpack.security.hacks.logoutNotification": "由于处于不活动状态,您即将退出。单击“确定”可以恢复。",
|
||||
"xpack.security.hacks.warningTitle": "警告",
|
||||
"xpack.security.loggedOut.login": "登录",
|
||||
|
@ -7532,7 +7527,6 @@
|
|||
"xpack.security.management.users.editUser.changingUserNameAfterCreationDescription": "用户名一经创建,将无法更改。",
|
||||
"xpack.security.management.users.editUser.confirmPasswordFormRowLabel": "确认密码",
|
||||
"xpack.security.management.users.editUser.createUserButtonLabel": "创建用户",
|
||||
"xpack.security.management.users.editUser.currentPasswordFormRowLabel": "当前密码",
|
||||
"xpack.security.management.users.editUser.deleteUserButtonLabel": "删除用户",
|
||||
"xpack.security.management.users.editUser.editUserTitle": "编辑 {userName} 用户",
|
||||
"xpack.security.management.users.editUser.emailAddressFormRowLabel": "电子邮件地址",
|
||||
|
@ -7541,7 +7535,6 @@
|
|||
"xpack.security.management.users.editUser.fullNameFormRowLabel": "全名",
|
||||
"xpack.security.management.users.editUser.incorrectPasswordErrorMessage": "您输入的当前密码不正确",
|
||||
"xpack.security.management.users.editUser.modifyingReservedUsersDescription": "保留的用户是内置的,无法删除或修改。只能更改密码。",
|
||||
"xpack.security.management.users.editUser.newPasswordFormRowLabel": "新密码",
|
||||
"xpack.security.management.users.editUser.newUserTitle": "新建用户",
|
||||
"xpack.security.management.users.editUser.passwordDoNotMatchErrorMessage": "密码不匹配",
|
||||
"xpack.security.management.users.editUser.passwordFormRowLabel": "密码",
|
||||
|
@ -7550,8 +7543,6 @@
|
|||
"xpack.security.management.users.editUser.requiredUsernameErrorMessage": "“用户名”必填",
|
||||
"xpack.security.management.users.editUser.returnToUserListButtonLabel": "返回到用户列表",
|
||||
"xpack.security.management.users.editUser.rolesFormRowLabel": "角色",
|
||||
"xpack.security.management.users.editUser.savePasswordButtonLabel": "保存密码",
|
||||
"xpack.security.management.users.editUser.savePasswordCancelButtonLabel": "取消",
|
||||
"xpack.security.management.users.editUser.savingUserErrorMessage": "保存用户时出错:{message}",
|
||||
"xpack.security.management.users.editUser.settingPasswordErrorMessage": "设置密码时出错:{message}",
|
||||
"xpack.security.management.users.editUser.updateUserButtonLabel": "更新用户",
|
||||
|
|
|
@ -15,21 +15,20 @@ export function AccountSettingProvider({ getService }) {
|
|||
async verifyAccountSettings(expectedEmail, expectedUserName) {
|
||||
await userMenu.clickProvileLink();
|
||||
|
||||
const usernameField = await testSubjects.find('usernameField');
|
||||
const usernameField = await testSubjects.find('username');
|
||||
const userName = await usernameField.getVisibleText();
|
||||
expect(userName).to.be(expectedUserName);
|
||||
|
||||
const emailIdField = await testSubjects.find('emailIdField');
|
||||
const emailIdField = await testSubjects.find('email');
|
||||
const emailField = await emailIdField.getVisibleText();
|
||||
expect(emailField).to.be(expectedEmail);
|
||||
}
|
||||
|
||||
async changePassword(currentPassword, newPassword) {
|
||||
await testSubjects.click('changePasswordLink');
|
||||
await testSubjects.setValue('newPasswordInput', newPassword);
|
||||
await testSubjects.setValue('currentPasswordInput', currentPassword);
|
||||
await testSubjects.setValue('confirmPasswordInput', newPassword);
|
||||
await testSubjects.click('saveChangesButton');
|
||||
await testSubjects.setValue('currentPassword', currentPassword);
|
||||
await testSubjects.setValue('newPassword', newPassword);
|
||||
await testSubjects.setValue('confirmNewPassword', newPassword);
|
||||
await testSubjects.click('changePasswordButton');
|
||||
await testSubjects.existOrFail('passwordUpdateSuccess');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue