From 8837d04e3587d8c83c0ef28c81acf3d118718e29 Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Thu, 11 Mar 2021 15:08:43 +0100 Subject: SONAR-14586 New form to secure the admin user account --- .../src/main/js/app/utils/startReactApp.tsx | 8 + .../ChangeAdminPasswordApp.tsx | 120 +++++++ .../ChangeAdminPasswordAppRenderer.tsx | 150 ++++++++ .../__tests__/ChangeAdminPasswordApp-test.tsx | 130 +++++++ .../ChangeAdminPasswordAppRenderer-test.tsx | 79 +++++ .../ChangeAdminPasswordApp-test.tsx.snap | 49 +++ .../ChangeAdminPasswordAppRenderer-test.tsx.snap | 377 +++++++++++++++++++++ .../js/apps/change-admin-password/constants.ts | 21 ++ server/sonar-web/src/main/js/types/types.d.ts | 1 + 9 files changed, 935 insertions(+) create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordAppRenderer-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap create mode 100644 server/sonar-web/src/main/js/apps/change-admin-password/constants.ts (limited to 'server/sonar-web') diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index 6455491dddd..65afaf311a4 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -324,6 +324,14 @@ export default function startReactApp( path="account/reset_password" component={lazyLoadComponent(() => import('../components/ResetPassword'))} /> + + import('../../apps/change-admin-password/ChangeAdminPasswordApp') + )} + /> import('../components/NotFound'))} diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx new file mode 100644 index 00000000000..1737206e6a1 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx @@ -0,0 +1,120 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { changePassword } from '../../api/users'; +import { Location, withRouter } from '../../components/hoc/withRouter'; +import { getAppState, Store } from '../../store/rootReducer'; +import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer'; +import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants'; + +interface Props { + canAdmin?: boolean; + instanceUsesDefaultAdminCredentials?: boolean; + location: Location; +} + +interface State { + passwordValue: string; + confirmPasswordValue: string; + canSubmit?: boolean; + submitting: boolean; + success: boolean; +} + +export class ChangeAdminPasswordApp extends React.PureComponent { + mounted = false; + + constructor(props: Props) { + super(props); + + this.state = { + passwordValue: '', + confirmPasswordValue: '', + submitting: false, + success: !props.instanceUsesDefaultAdminCredentials + }; + } + + componentDidMount() { + this.mounted = true; + } + + componentWillUnmount() { + this.mounted = false; + } + + handlePasswordChange = (passwordValue: string) => { + this.setState({ passwordValue }, this.checkCanSubmit); + }; + + handleConfirmPasswordChange = (confirmPasswordValue: string) => { + this.setState({ confirmPasswordValue }, this.checkCanSubmit); + }; + + handleSubmit = async () => { + const { canSubmit, passwordValue } = this.state; + if (canSubmit) { + this.setState({ submitting: true }); + const success = await changePassword({ + login: DEFAULT_ADMIN_LOGIN, + password: passwordValue + }).then( + () => true, + () => false + ); + if (this.mounted) { + this.setState({ submitting: false, success }); + } + } + }; + + checkCanSubmit = () => { + this.setState(({ passwordValue, confirmPasswordValue }) => ({ + canSubmit: passwordValue === confirmPasswordValue && passwordValue !== DEFAULT_ADMIN_PASSWORD + })); + }; + + render() { + const { canAdmin, location } = this.props; + const { canSubmit, confirmPasswordValue, passwordValue, submitting, success } = this.state; + return ( + + ); + } +} + +export const mapStateToProps = (state: Store) => { + const { canAdmin, instanceUsesDefaultAdminCredentials } = getAppState(state); + return { canAdmin, instanceUsesDefaultAdminCredentials }; +}; + +export default connect(mapStateToProps)(withRouter(ChangeAdminPasswordApp)); diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx new file mode 100644 index 00000000000..0f01456ccba --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx @@ -0,0 +1,150 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { SubmitButton } from 'sonar-ui-common/components/controls/buttons'; +import { Alert } from 'sonar-ui-common/components/ui/Alert'; +import MandatoryFieldMarker from 'sonar-ui-common/components/ui/MandatoryFieldMarker'; +import MandatoryFieldsExplanation from 'sonar-ui-common/components/ui/MandatoryFieldsExplanation'; +import { translate } from 'sonar-ui-common/helpers/l10n'; +import { getReturnUrl } from 'sonar-ui-common/helpers/urls'; +import GlobalMessagesContainer from '../../app/components/GlobalMessagesContainer'; +import { Location } from '../../components/hoc/withRouter'; +import Unauthorized from '../sessions/components/Unauthorized'; +import { DEFAULT_ADMIN_PASSWORD } from './constants'; + +export interface ChangeAdminPasswordAppRendererProps { + passwordValue: string; + confirmPasswordValue: string; + onConfirmPasswordChange: (password: string) => void; + onPasswordChange: (password: string) => void; + onSubmit: () => void; + canAdmin?: boolean; + canSubmit?: boolean; + submitting: boolean; + success: boolean; + location: Location; +} + +export default function ChangeAdminPasswordAppRenderer(props: ChangeAdminPasswordAppRendererProps) { + const { + canAdmin, + canSubmit, + confirmPasswordValue, + passwordValue, + location, + submitting, + success + } = props; + + if (!canAdmin) { + return ; + } + + return ( +
+
+ {success ? ( + +

{translate('users.change_admin_password.form.success')}

+ {/* We must not use Link here, because we need a refresh of the /api/navigation/global call. */} + + {translate('users.change_admin_password.form.continue_to_app')} + +
+ ) : ( + <> + + +

+ {translate('users.change_admin_password.instance_is_at_risk')} +

+

+ {translate('users.change_admin_password.header')} +

+

+ {translate('users.change_admin_password.description')} +

+ +
) => { + e.preventDefault(); + props.onSubmit(); + }}> +

+ {translate('users.change_admin_password.form.header')} +

+ + + +
+ + ) => { + props.onPasswordChange(e.currentTarget.value); + }} + required={true} + type="password" + value={passwordValue} + /> +
+ +
+ + ) => { + props.onConfirmPasswordChange(e.currentTarget.value); + }} + required={true} + type="password" + value={confirmPasswordValue} + /> + + {confirmPasswordValue === passwordValue && + passwordValue === DEFAULT_ADMIN_PASSWORD && ( + + {translate('users.change_admin_password.form.cannot_use_default_password')} + + )} +
+ +
+ + {translate('change_verb')} + {submitting && } + +
+ + + )} +
+
+ ); +} diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx new file mode 100644 index 00000000000..efc3e980c75 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx @@ -0,0 +1,130 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; +import { changePassword } from '../../../api/users'; +import { mockLocation } from '../../../helpers/testMocks'; +import { getAppState, Store } from '../../../store/rootReducer'; +import { ChangeAdminPasswordApp, mapStateToProps } from '../ChangeAdminPasswordApp'; +import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from '../constants'; + +jest.mock('react-redux', () => ({ + connect: jest.fn(() => (a: any) => a) +})); + +jest.mock('../../../store/rootReducer', () => ({ + ...jest.requireActual('../../../store/rootReducer'), + getAppState: jest.fn() +})); + +jest.mock('../../../api/users', () => ({ + changePassword: jest.fn().mockResolvedValue(null) +})); + +beforeEach(jest.clearAllMocks); + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ instanceUsesDefaultAdminCredentials: undefined })).toMatchSnapshot( + 'admin is not using the default password' + ); +}); + +it('should correctly handle input changes', () => { + const wrapper = shallowRender(); + + // Set different values; should not allow submission. + wrapper.instance().handlePasswordChange('new pass'); + wrapper.instance().handleConfirmPasswordChange('confirm pass'); + + expect(wrapper.state().passwordValue).toBe('new pass'); + expect(wrapper.state().confirmPasswordValue).toBe('confirm pass'); + expect(wrapper.state().canSubmit).toBe(false); + + // Set the same values; should allow submission. + wrapper.instance().handleConfirmPasswordChange('new pass'); + expect(wrapper.state().canSubmit).toBe(true); + + // Set the default admin password; should not allow submission. + wrapper.instance().handlePasswordChange(DEFAULT_ADMIN_PASSWORD); + expect(wrapper.state().canSubmit).toBe(false); +}); + +it('should correctly update the password', async () => { + (changePassword as jest.Mock).mockResolvedValueOnce(null).mockRejectedValueOnce(null); + const wrapper = shallowRender(); + wrapper.setState({ canSubmit: false, passwordValue: 'new pass' }); + + wrapper.instance().handleSubmit(); + expect(wrapper.state().submitting).toBe(false); + expect(changePassword).not.toBeCalled(); + + wrapper.setState({ canSubmit: true }); + wrapper.instance().handleSubmit(); + expect(wrapper.state().submitting).toBe(true); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().submitting).toBe(false); + expect(wrapper.state().success).toBe(true); + expect(changePassword).toBeCalledWith({ + login: DEFAULT_ADMIN_LOGIN, + password: 'new pass' + }); + + wrapper.instance().handleSubmit(); + expect(wrapper.state().submitting).toBe(true); + + await waitAndUpdate(wrapper); + + expect(wrapper.state().submitting).toBe(false); + expect(wrapper.state().success).toBe(false); +}); + +describe('redux', () => { + it('should correctly map state props', () => { + (getAppState as jest.Mock) + .mockReturnValueOnce({}) + .mockReturnValueOnce({ canAdmin: false, instanceUsesDefaultAdminCredentials: true }); + + expect(mapStateToProps({} as Store)).toEqual({ + canAdmin: undefined, + instanceUsesDefaultAdminCredentials: undefined + }); + + expect(mapStateToProps({} as Store)).toEqual({ + canAdmin: false, + instanceUsesDefaultAdminCredentials: true + }); + }); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordAppRenderer-test.tsx b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordAppRenderer-test.tsx new file mode 100644 index 00000000000..4861353e498 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordAppRenderer-test.tsx @@ -0,0 +1,79 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { shallow } from 'enzyme'; +import * as React from 'react'; +import { change, submit } from 'sonar-ui-common/helpers/testUtils'; +import { mockLocation } from '../../../helpers/testMocks'; +import ChangeAdminPasswordAppRenderer, { + ChangeAdminPasswordAppRendererProps +} from '../ChangeAdminPasswordAppRenderer'; +import { DEFAULT_ADMIN_PASSWORD } from '../constants'; + +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); + expect(shallowRender({ canAdmin: false })).toMatchSnapshot('access denied'); + expect(shallowRender({ canSubmit: false })).toMatchSnapshot('cannot submit'); + expect(shallowRender({ submitting: true })).toMatchSnapshot('submitting'); + expect( + shallowRender({ + passwordValue: DEFAULT_ADMIN_PASSWORD, + confirmPasswordValue: DEFAULT_ADMIN_PASSWORD + }) + ).toMatchSnapshot('trying to use default admin password'); + expect(shallowRender({ success: true })).toMatchSnapshot('success'); +}); + +it('should correctly react to input changes', () => { + const onConfirmPasswordChange = jest.fn(); + const onPasswordChange = jest.fn(); + const wrapper = shallowRender({ onConfirmPasswordChange, onPasswordChange }); + + change(wrapper.find('#user-password'), 'new pass'); + change(wrapper.find('#confirm-user-password'), 'confirm pass'); + expect(onPasswordChange).toBeCalledWith('new pass'); + expect(onConfirmPasswordChange).toBeCalledWith('confirm pass'); +}); + +it('should correctly submit the form', () => { + const onSubmit = jest.fn(); + const wrapper = shallowRender({ onSubmit }); + + submit(wrapper.find('form')); + expect(onSubmit).toBeCalled(); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap new file mode 100644 index 00000000000..5371a8f4386 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: admin is not using the default password 1`] = ` + +`; + +exports[`should render correctly: default 1`] = ` + +`; diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap new file mode 100644 index 00000000000..5c5895a5782 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap @@ -0,0 +1,377 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly: access denied 1`] = ``; + +exports[`should render correctly: cannot submit 1`] = ` +
+
+ +

+ users.change_admin_password.instance_is_at_risk +

+

+ users.change_admin_password.header +

+

+ users.change_admin_password.description +

+
+

+ users.change_admin_password.form.header +

+ +
+ + +
+
+ + +
+
+ + change_verb + +
+ +
+
+`; + +exports[`should render correctly: default 1`] = ` +
+
+ +

+ users.change_admin_password.instance_is_at_risk +

+

+ users.change_admin_password.header +

+

+ users.change_admin_password.description +

+
+

+ users.change_admin_password.form.header +

+ +
+ + +
+
+ + +
+
+ + change_verb + +
+ +
+
+`; + +exports[`should render correctly: submitting 1`] = ` +
+
+ +

+ users.change_admin_password.instance_is_at_risk +

+

+ users.change_admin_password.header +

+

+ users.change_admin_password.description +

+
+

+ users.change_admin_password.form.header +

+ +
+ + +
+
+ + +
+
+ + change_verb + + +
+ +
+
+`; + +exports[`should render correctly: success 1`] = ` +
+
+ +

+ users.change_admin_password.form.success +

+ + users.change_admin_password.form.continue_to_app + +
+
+
+`; + +exports[`should render correctly: trying to use default admin password 1`] = ` +
+
+ +

+ users.change_admin_password.instance_is_at_risk +

+

+ users.change_admin_password.header +

+

+ users.change_admin_password.description +

+
+

+ users.change_admin_password.form.header +

+ +
+ + +
+
+ + + + users.change_admin_password.form.cannot_use_default_password + +
+
+ + change_verb + +
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/apps/change-admin-password/constants.ts b/server/sonar-web/src/main/js/apps/change-admin-password/constants.ts new file mode 100644 index 00000000000..eecbed8b92f --- /dev/null +++ b/server/sonar-web/src/main/js/apps/change-admin-password/constants.ts @@ -0,0 +1,21 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +export const DEFAULT_ADMIN_LOGIN = 'admin'; +export const DEFAULT_ADMIN_PASSWORD = 'admin'; diff --git a/server/sonar-web/src/main/js/types/types.d.ts b/server/sonar-web/src/main/js/types/types.d.ts index 223967cd14e..404c070c28f 100644 --- a/server/sonar-web/src/main/js/types/types.d.ts +++ b/server/sonar-web/src/main/js/types/types.d.ts @@ -90,6 +90,7 @@ declare namespace T { canAdmin?: boolean; edition: 'community' | 'developer' | 'enterprise' | 'datacenter' | undefined; globalPages?: Extension[]; + instanceUsesDefaultAdminCredentials?: boolean; multipleAlmEnabled?: boolean; needIssueSync?: boolean; productionDatabase: boolean; -- cgit v1.2.3