From a70a2cdc001ddce3db142d2cdd382a1da75611be Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 15 Apr 2019 15:48:32 -0700 Subject: [PATCH] Revert "Convert account screen to React/EUI (#30977)" This reverts commit 031682d7d04a8b27d63762c56cd697e10f0cc5c7. --- x-pack/plugins/security/common/constants.ts | 1 + .../security/common/model/user.test.ts | 62 ---- x-pack/plugins/security/common/model/user.ts | 37 -- .../change_password_form.test.tsx | 111 ------ .../change_password_form.tsx | 327 ------------------ .../management/change_password_form/index.ts | 7 - .../management/users/confirm_delete.js | 5 +- .../components/management/users/edit_user.js | 104 ++++-- .../components/management/users/users.js | 11 +- x-pack/plugins/security/public/lib/api.js | 55 +++ x-pack/plugins/security/public/lib/api.ts | 60 ---- .../public/views/account/account.html | 55 ++- .../security/public/views/account/account.js | 67 ++-- .../account_management_page.test.tsx | 70 ---- .../components/account_management_page.tsx | 48 --- .../change_password/change_password.tsx | 76 ---- .../components/change_password/index.ts | 7 - .../public/views/account/components/index.ts | 7 - .../account/components/personal_info/index.ts | 7 - .../personal_info/personal_info.tsx | 58 ---- .../public/views/management/edit_user.js | 2 + .../security/public/views/management/users.js | 5 +- .../translations/translations/zh-CN.json | 9 + .../page_objects/accountsetting_page.js | 13 +- 24 files changed, 256 insertions(+), 948 deletions(-) delete mode 100644 x-pack/plugins/security/common/model/user.test.ts delete mode 100644 x-pack/plugins/security/common/model/user.ts delete mode 100644 x-pack/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx delete mode 100644 x-pack/plugins/security/public/components/management/change_password_form/change_password_form.tsx delete mode 100644 x-pack/plugins/security/public/components/management/change_password_form/index.ts create mode 100644 x-pack/plugins/security/public/lib/api.js delete mode 100644 x-pack/plugins/security/public/lib/api.ts delete mode 100644 x-pack/plugins/security/public/views/account/components/account_management_page.test.tsx delete mode 100644 x-pack/plugins/security/public/views/account/components/account_management_page.tsx delete mode 100644 x-pack/plugins/security/public/views/account/components/change_password/change_password.tsx delete mode 100644 x-pack/plugins/security/public/views/account/components/change_password/index.ts delete mode 100644 x-pack/plugins/security/public/views/account/components/index.ts delete mode 100644 x-pack/plugins/security/public/views/account/components/personal_info/index.ts delete mode 100644 x-pack/plugins/security/public/views/account/components/personal_info/personal_info.tsx diff --git a/x-pack/plugins/security/common/constants.ts b/x-pack/plugins/security/common/constants.ts index 2a255ecd335e50..bca0684209ba11 100644 --- a/x-pack/plugins/security/common/constants.ts +++ b/x-pack/plugins/security/common/constants.ts @@ -6,5 +6,6 @@ export const GLOBAL_RESOURCE = '*'; export const IGNORED_TYPES = ['space']; +export const REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE = ['reserved', 'native']; export const APPLICATION_PREFIX = 'kibana-'; export const RESERVED_PRIVILEGES_APPLICATION_WILDCARD = 'kibana-*'; diff --git a/x-pack/plugins/security/common/model/user.test.ts b/x-pack/plugins/security/common/model/user.test.ts deleted file mode 100644 index 3f15ca3cf1ab3b..00000000000000 --- a/x-pack/plugins/security/common/model/user.test.ts +++ /dev/null @@ -1,62 +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 { 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); - }); -}); diff --git a/x-pack/plugins/security/common/model/user.ts b/x-pack/plugins/security/common/model/user.ts deleted file mode 100644 index 007661062c991f..00000000000000 --- a/x-pack/plugins/security/common/model/user.ts +++ /dev/null @@ -1,37 +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. - */ - -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); -} diff --git a/x-pack/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx deleted file mode 100644 index 8d2adfd78884da..00000000000000 --- a/x-pack/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx +++ /dev/null @@ -1,111 +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. - */ -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) { - return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]'); -} - -function getNewPasswordField(wrapper: ReactWrapper) { - return wrapper.find(EuiFieldText).filter('[data-test-subj="newPassword"]'); -} - -function getConfirmPasswordField(wrapper: ReactWrapper) { - return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]'); -} - -describe('', () => { - 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( - - ); - - 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( - - ); - - 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( - - ); - - expect(getCurrentPasswordField(wrapper)).toHaveLength(0); - expect(getNewPasswordField(wrapper)).toHaveLength(1); - expect(getConfirmPasswordField(wrapper)).toHaveLength(1); - }); - }); -}); diff --git a/x-pack/plugins/security/public/components/management/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/components/management/change_password_form/change_password_form.tsx deleted file mode 100644 index c83dab2d7a50e2..00000000000000 --- a/x-pack/plugins/security/public/components/management/change_password_form/change_password_form.tsx +++ /dev/null @@ -1,327 +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 { - 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 { - constructor(props: Props) { - super(props); - this.state = getInitialState(); - } - - public render() { - return this.getForm(); - } - - private getForm = () => { - return ( - - {this.props.isUserChangingOwnPassword && ( - - } - > - - - )} - - - } - {...this.validateNewPassword()} - fullWidth - label={ - - } - > - - - - } - > - - - - - - - - - - - - - - - - - - ); - }; - - private onCurrentPasswordChange = (e: ChangeEvent) => { - this.setState({ currentPassword: e.target.value, currentPasswordError: false }); - }; - - private onNewPasswordChange = (e: ChangeEvent) => { - this.setState({ newPassword: e.target.value }); - }; - - private onConfirmPasswordChange = (e: ChangeEvent) => { - 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: ( - - ), - }; - } - - if (!this.state.currentPassword) { - return { - isInvalid: true, - error: ( - - ), - }; - } - - 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: ( - - ), - }; - } - - return { - isInvalid: false, - }; - }; - - private validateConfirmPassword = (shouldValidate = this.state.shouldValidate) => { - const { newPassword, confirmPassword } = this.state; - if (shouldValidate && newPassword !== confirmPassword) { - return { - isInvalid: true, - error: ( - - ), - }; - } - - 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) => { - 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') }, - }) - ); - } - }; -} diff --git a/x-pack/plugins/security/public/components/management/change_password_form/index.ts b/x-pack/plugins/security/public/components/management/change_password_form/index.ts deleted file mode 100644 index 808ca4495ac946..00000000000000 --- a/x-pack/plugins/security/public/components/management/change_password_form/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { ChangePasswordForm } from './change_password_form'; diff --git a/x-pack/plugins/security/public/components/management/users/confirm_delete.js b/x-pack/plugins/security/public/components/management/users/confirm_delete.js index 887db0a1235931..2b5b78084a8ed9 100644 --- a/x-pack/plugins/security/public/components/management/users/confirm_delete.js +++ b/x-pack/plugins/security/public/components/management/users/confirm_delete.js @@ -8,15 +8,14 @@ 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, callback } = this.props; + const { usersToDelete, apiClient, callback } = this.props; const errors = []; usersToDelete.forEach(async username => { try { - await UserAPIClient.deleteUser(username); + await apiClient.deleteUser(username); toastNotifications.addSuccess( this.props.intl.formatMessage({ id: 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', diff --git a/x-pack/plugins/security/public/components/management/users/edit_user.js b/x-pack/plugins/security/public/components/management/users/edit_user.js index bf13d59cc54dea..d3ce0d4da62477 100644 --- a/x-pack/plugins/security/public/components/management/users/edit_user.js +++ b/x-pack/plugins/security/public/components/management/users/edit_user.js @@ -31,8 +31,6 @@ 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_@\-\$\.]*/; @@ -57,35 +55,36 @@ class EditUserUI extends Component { }; } async componentDidMount() { - const { username } = this.props; + const { apiClient, username } = this.props; let { user, currentUser } = this.state; if (username) { try { - user = await UserAPIClient.getUser(username); - currentUser = await UserAPIClient.getCurrentUser(); + user = await apiClient.getUser(username); + currentUser = await apiClient.getCurrentUser(); } catch (err) { toastNotifications.addDanger({ title: this.props.intl.formatMessage({ id: 'xpack.security.management.users.editUser.errorLoadingUserTitle', defaultMessage: 'Error loading user' }), - text: get(err, 'body.message') || err.message, + text: get(err, 'data.message') || err.message, }); return; } } - let roles = []; + let roles; try { - roles = await UserAPIClient.getRoles(); + roles = await apiClient.getRoles(); } catch (err) { toastNotifications.addDanger({ title: this.props.intl.formatMessage({ id: 'xpack.security.management.users.editUser.errorLoadingRolesTitle', defaultMessage: 'Error loading roles' }), - text: get(err, 'body.message') || err.message, + text: get(err, 'data.message') || err.message, }); + return; } this.setState({ @@ -154,9 +153,10 @@ class EditUserUI extends Component { } }; changePassword = async () => { + const { apiClient } = this.props; const { user, password, currentPassword } = this.state; try { - await UserAPIClient.changePassword(user.username, password, currentPassword); + await apiClient.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.body.statusCode === 401) { + if (e.status === 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: get(e, 'body.message', 'Unknown error') }) + }, { message: e.data.message }) ); } } this.clearPasswordForm(); }; saveUser = async () => { - const { changeUrl } = this.props; + const { apiClient, 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 UserAPIClient.saveUser(userToSave); + await apiClient.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: get(e, 'body.message', 'Unknown error') }) + }, { message: e.data.message }) ); } }; @@ -213,11 +213,32 @@ class EditUserUI extends Component { }); }; passwordFields = () => { + const { user, currentUser } = this.state; + const userIsLoggedInUser = user.username && user.username === currentUser.username; return ( + {userIsLoggedInUser ? ( + + this.setState({ currentPassword: event.target.value })} + /> + + ) : null} { const { showChangePasswordForm, - user, - currentUser, + password, + confirmPassword, + user: { username }, } = this.state; - - const userIsLoggedInUser = user.username && user.username === currentUser.username; - if (!showChangePasswordForm) { return null; } return ( - {user.username === 'kibana' ? ( + {this.passwordFields()} + {username === 'kibana' ? ( ) : null} - + + + { + this.changePassword(password); + }} + > + + + + + { + this.clearPasswordForm(); + }} + > + + + + ); }; @@ -318,7 +365,7 @@ class EditUserUI extends Component { this.setState({ showDeleteConfirmation: false }); }; render() { - const { changeUrl, intl } = this.props; + const { changeUrl, apiClient, intl } = this.props; const { user, roles, @@ -380,6 +427,7 @@ class EditUserUI extends Component { {showDeleteConfirmation ? ( diff --git a/x-pack/plugins/security/public/components/management/users/users.js b/x-pack/plugins/security/public/components/management/users/users.js index baa7f9a3056d51..8a526c70355a7b 100644 --- a/x-pack/plugins/security/public/components/management/users/users.js +++ b/x-pack/plugins/security/public/components/management/users/users.js @@ -20,7 +20,6 @@ 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) { @@ -45,18 +44,19 @@ class UsersUI extends Component { }); }; async loadUsers() { + const { apiClient } = this.props; try { - const users = await UserAPIClient.getUsers(); + const users = await apiClient.getUsers(); this.setState({ users }); } catch (e) { - if (e.body.statusCode === 403) { + if (e.status === 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.body.message }) + }, { message: e.data.message }) ); } } @@ -88,7 +88,7 @@ class UsersUI extends Component { } render() { const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state; - const { intl } = this.props; + const { apiClient, intl } = this.props; if (permissionDenied) { return (
@@ -251,6 +251,7 @@ class UsersUI extends Component { {showDeleteConfirmation ? ( user.username)} callback={this.handleDelete} /> diff --git a/x-pack/plugins/security/public/lib/api.js b/x-pack/plugins/security/public/lib/api.js new file mode 100644 index 00000000000000..a41afcc0d4a3f8 --- /dev/null +++ b/x-pack/plugins/security/public/lib/api.js @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import chrome from 'ui/chrome'; + +const usersUrl = chrome.addBasePath('/api/security/v1/users'); +const rolesUrl = chrome.addBasePath('/api/security/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); + } + }; +}; diff --git a/x-pack/plugins/security/public/lib/api.ts b/x-pack/plugins/security/public/lib/api.ts deleted file mode 100644 index 14128942dea6cf..00000000000000 --- a/x-pack/plugins/security/public/lib/api.ts +++ /dev/null @@ -1,60 +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 { 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 { - return await kfetch({ pathname: `/api/security/v1/me` }); - } - - public static async getUsers(): Promise { - return await kfetch({ pathname: usersUrl }); - } - - public static async getUser(username: string): Promise { - 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 { - return await kfetch({ pathname: rolesUrl }); - } - - public static async getRole(name: string): Promise { - const url = `${rolesUrl}/${encodeURIComponent(name)}`; - return await kfetch({ pathname: url }); - } - - public static async changePassword(username: string, password: string, currentPassword: string) { - const data: Record = { - newPassword: password, - }; - if (currentPassword) { - data.password = currentPassword; - } - await kfetch({ - pathname: `${usersUrl}/${encodeURIComponent(username)}/password`, - method: 'POST', - body: JSON.stringify(data), - }); - } -} diff --git a/x-pack/plugins/security/public/views/account/account.html b/x-pack/plugins/security/public/views/account/account.html index 0935c415b18295..d0ba4b2681883a 100644 --- a/x-pack/plugins/security/public/views/account/account.html +++ b/x-pack/plugins/security/public/views/account/account.html @@ -1 +1,54 @@ -
+
+ +
+ +
+

+
+ + +
+
+ + +
+ +
+

+
+
+ + +
+ +
+

+
+
+ + +
+ +
+

+
+
+ +
diff --git a/x-pack/plugins/security/public/views/account/account.js b/x-pack/plugins/security/public/views/account/account.js index fcf5364d5f3f3b..d2ab51d0caa44b 100644 --- a/x-pack/plugins/security/public/views/account/account.js +++ b/x-pack/plugins/security/public/views/account/account.js @@ -4,32 +4,21 @@ * 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 { I18nContext } from 'ui/i18n'; -import { AccountManagementPage } from './components'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; - -const renderReact = (elem, user) => { - render( - - - , - elem - ); -}; +import { REALMS_ELIGIBLE_FOR_PASSWORD_CHANGE } from '../../../common/constants'; routes.when('/account', { template, k7Breadcrumbs: () => [ { text: i18n.translate('xpack.security.account.breadcrumb', { - defaultMessage: 'Account Management', + defaultMessage: 'Account', }) } ], @@ -39,16 +28,42 @@ routes.when('/account', { } }, controllerAs: 'accountController', - controller($scope, $route) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('userProfileReactRoot'); - if (elem) { - unmountComponentAtNode(elem); + 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; } - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('userProfileReactRoot'); - renderReact(elem, $route.current.locals.user); - }); + + $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)' + }); + }; } }); diff --git a/x-pack/plugins/security/public/views/account/components/account_management_page.test.tsx b/x-pack/plugins/security/public/views/account/components/account_management_page.test.tsx deleted file mode 100644 index cdc67cb918d727..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/account_management_page.test.tsx +++ /dev/null @@ -1,70 +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 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('', () => { - it(`displays users full name, username, and email address`, () => { - const user: User = createUser(); - const wrapper = mountWithIntl(); - 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(); - 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(); - 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(); - 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(); - expect(wrapper.find('EuiFieldText[data-test-subj="currentPassword"]')).toHaveLength(0); - expect(wrapper.find('EuiFieldText[data-test-subj="newPassword"]')).toHaveLength(0); - }); -}); diff --git a/x-pack/plugins/security/public/views/account/components/account_management_page.tsx b/x-pack/plugins/security/public/views/account/components/account_management_page.tsx deleted file mode 100644 index 43f145a1e15c02..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/account_management_page.tsx +++ /dev/null @@ -1,48 +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 { - // @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 { - constructor(props: Props) { - super(props); - } - - public render() { - return ( - - - - -

{getUserDisplayName(this.props.user)}

-
- - - - - - -
-
-
- ); - } -} diff --git a/x-pack/plugins/security/public/views/account/components/change_password/change_password.tsx b/x-pack/plugins/security/public/views/account/components/change_password/change_password.tsx deleted file mode 100644 index b92897489e3292..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/change_password/change_password.tsx +++ /dev/null @@ -1,76 +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 { - // @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 { - constructor(props: Props) { - super(props); - } - - public render() { - const canChangePassword = canUserChangePassword(this.props.user); - - const changePasswordTitle = ( - - ); - - if (canChangePassword) { - return this.getChangePasswordForm(changePasswordTitle); - } - return this.getChangePasswordUnavailable(changePasswordTitle); - } - - private getChangePasswordForm = (changePasswordTitle: React.ReactElement) => { - return ( - {changePasswordTitle}} - description={ -

- -

- } - > - -
- ); - }; - - private getChangePasswordUnavailable(changePasswordTitle: React.ReactElement) { - return ( - {changePasswordTitle}} - description={ -

- -

- } - > -
- - ); - } -} diff --git a/x-pack/plugins/security/public/views/account/components/change_password/index.ts b/x-pack/plugins/security/public/views/account/components/change_password/index.ts deleted file mode 100644 index ccd810bb814c00..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/change_password/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { ChangePassword } from './change_password'; diff --git a/x-pack/plugins/security/public/views/account/components/index.ts b/x-pack/plugins/security/public/views/account/components/index.ts deleted file mode 100644 index 0f119b7cc0b1d8..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { AccountManagementPage } from './account_management_page'; diff --git a/x-pack/plugins/security/public/views/account/components/personal_info/index.ts b/x-pack/plugins/security/public/views/account/components/personal_info/index.ts deleted file mode 100644 index 5980157f5b76e2..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/personal_info/index.ts +++ /dev/null @@ -1,7 +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. - */ - -export { PersonalInfo } from './personal_info'; diff --git a/x-pack/plugins/security/public/views/account/components/personal_info/personal_info.tsx b/x-pack/plugins/security/public/views/account/components/personal_info/personal_info.tsx deleted file mode 100644 index 2c4058851451da..00000000000000 --- a/x-pack/plugins/security/public/views/account/components/personal_info/personal_info.tsx +++ /dev/null @@ -1,58 +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 { - // @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 ( - - - - } - description={ - - } - > - - -
-
- {props.user.username} -
-
- {props.user.email || ( - - )} -
-
-
-
-
- ); -}; diff --git a/x-pack/plugins/security/public/views/management/edit_user.js b/x-pack/plugins/security/public/views/management/edit_user.js index 147d451b15ef9c..23da6db3fb1b3b 100644 --- a/x-pack/plugins/security/public/views/management/edit_user.js +++ b/x-pack/plugins/security/public/views/management/edit_user.js @@ -13,6 +13,7 @@ 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'; @@ -21,6 +22,7 @@ const renderReact = (elem, httpClient, changeUrl, username) => { , diff --git a/x-pack/plugins/security/public/views/management/users.js b/x-pack/plugins/security/public/views/management/users.js index d29bad4e1d2d2f..5ce5c0c81314fb 100644 --- a/x-pack/plugins/security/public/views/management/users.js +++ b/x-pack/plugins/security/public/views/management/users.js @@ -11,6 +11,7 @@ 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'; @@ -18,8 +19,8 @@ routes.when(SECURITY_PATH, { redirectTo: USERS_PATH, }); -const renderReact = (elem, changeUrl) => { - render(, elem); +const renderReact = (elem, httpClient, changeUrl) => { + render(, elem); }; routes.when(USERS_PATH, { diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b96739708ff37c..5e59b9b69c42f0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7403,8 +7403,13 @@ "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": "登录", @@ -7527,6 +7532,7 @@ "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": "电子邮件地址", @@ -7535,6 +7541,7 @@ "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": "密码", @@ -7543,6 +7550,8 @@ "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": "更新用户", diff --git a/x-pack/test/functional/page_objects/accountsetting_page.js b/x-pack/test/functional/page_objects/accountsetting_page.js index 27f703a5dd427d..a49b9544ce7e30 100644 --- a/x-pack/test/functional/page_objects/accountsetting_page.js +++ b/x-pack/test/functional/page_objects/accountsetting_page.js @@ -15,20 +15,21 @@ export function AccountSettingProvider({ getService }) { async verifyAccountSettings(expectedEmail, expectedUserName) { await userMenu.clickProvileLink(); - const usernameField = await testSubjects.find('username'); + const usernameField = await testSubjects.find('usernameField'); const userName = await usernameField.getVisibleText(); expect(userName).to.be(expectedUserName); - const emailIdField = await testSubjects.find('email'); + const emailIdField = await testSubjects.find('emailIdField'); const emailField = await emailIdField.getVisibleText(); expect(emailField).to.be(expectedEmail); } async changePassword(currentPassword, newPassword) { - await testSubjects.setValue('currentPassword', currentPassword); - await testSubjects.setValue('newPassword', newPassword); - await testSubjects.setValue('confirmNewPassword', newPassword); - await testSubjects.click('changePasswordButton'); + 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.existOrFail('passwordUpdateSuccess'); } }