]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14586 New form to secure the admin user account
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Thu, 11 Mar 2021 14:08:43 +0000 (15:08 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 18 Mar 2021 20:08:12 +0000 (20:08 +0000)
server/sonar-web/src/main/js/app/utils/startReactApp.tsx
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordAppRenderer-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordApp-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/__snapshots__/ChangeAdminPasswordAppRenderer-test.tsx.snap [new file with mode: 0644]
server/sonar-web/src/main/js/apps/change-admin-password/constants.ts [new file with mode: 0644]
server/sonar-web/src/main/js/types/types.d.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 6455491ddddc89a545974815559957d87ed851a6..65afaf311a4dac89b0a4e5583f047b0248c1ec78 100644 (file)
@@ -324,6 +324,14 @@ export default function startReactApp(
                     path="account/reset_password"
                     component={lazyLoadComponent(() => import('../components/ResetPassword'))}
                   />
+                  <Route
+                    // We don't want this route to have any menu. This is why we define it here
+                    // rather than under the admin routes.
+                    path="admin/change_admin_password"
+                    component={lazyLoadComponent(() =>
+                      import('../../apps/change-admin-password/ChangeAdminPasswordApp')
+                    )}
+                  />
                   <Route
                     path="not_found"
                     component={lazyLoadComponent(() => 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 (file)
index 0000000..1737206
--- /dev/null
@@ -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<Props, State> {
+  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 (
+      <ChangeAdminPasswordAppRenderer
+        canAdmin={canAdmin}
+        passwordValue={passwordValue}
+        confirmPasswordValue={confirmPasswordValue}
+        canSubmit={canSubmit}
+        onPasswordChange={this.handlePasswordChange}
+        onConfirmPasswordChange={this.handleConfirmPasswordChange}
+        onSubmit={this.handleSubmit}
+        submitting={submitting}
+        success={success}
+        location={location}
+      />
+    );
+  }
+}
+
+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 (file)
index 0000000..0f01456
--- /dev/null
@@ -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 <Unauthorized />;
+  }
+
+  return (
+    <div className="page-wrapper-simple">
+      <div className="page-simple">
+        {success ? (
+          <Alert variant="success">
+            <p className="spacer-bottom">{translate('users.change_admin_password.form.success')}</p>
+            {/* We must not use Link here, because we need a refresh of the /api/navigation/global call. */}
+            <a href={getReturnUrl(location)}>
+              {translate('users.change_admin_password.form.continue_to_app')}
+            </a>
+          </Alert>
+        ) : (
+          <>
+            <GlobalMessagesContainer />
+
+            <h1 className="text-center bg-danger big padded">
+              {translate('users.change_admin_password.instance_is_at_risk')}
+            </h1>
+            <p className="text-center huge huge-spacer-top">
+              {translate('users.change_admin_password.header')}
+            </p>
+            <p className="text-center huge-spacer-top huge-spacer-bottom">
+              {translate('users.change_admin_password.description')}
+            </p>
+
+            <form
+              className="text-center"
+              onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
+                e.preventDefault();
+                props.onSubmit();
+              }}>
+              <h2 className="big-spacer-bottom big">
+                {translate('users.change_admin_password.form.header')}
+              </h2>
+
+              <MandatoryFieldsExplanation className="form-field" />
+
+              <div className="form-field">
+                <label htmlFor="user-password">
+                  {translate('users.change_admin_password.form.password')}
+                  <MandatoryFieldMarker />
+                </label>
+                <input
+                  id="user-password"
+                  name="password"
+                  onChange={(e: React.SyntheticEvent<HTMLInputElement>) => {
+                    props.onPasswordChange(e.currentTarget.value);
+                  }}
+                  required={true}
+                  type="password"
+                  value={passwordValue}
+                />
+              </div>
+
+              <div className="form-field">
+                <label htmlFor="confirm-user-password">
+                  {translate('users.change_admin_password.form.confirm')}
+                  <MandatoryFieldMarker />
+                </label>
+                <input
+                  id="confirm-user-password"
+                  name="confirm-password"
+                  onChange={(e: React.SyntheticEvent<HTMLInputElement>) => {
+                    props.onConfirmPasswordChange(e.currentTarget.value);
+                  }}
+                  required={true}
+                  type="password"
+                  value={confirmPasswordValue}
+                />
+
+                {confirmPasswordValue === passwordValue &&
+                  passwordValue === DEFAULT_ADMIN_PASSWORD && (
+                    <Alert className="spacer-top" variant="warning">
+                      {translate('users.change_admin_password.form.cannot_use_default_password')}
+                    </Alert>
+                  )}
+              </div>
+
+              <div className="form-field">
+                <SubmitButton disabled={!canSubmit || submitting}>
+                  {translate('change_verb')}
+                  {submitting && <i className="spinner spacer-left" />}
+                </SubmitButton>
+              </div>
+            </form>
+          </>
+        )}
+      </div>
+    </div>
+  );
+}
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 (file)
index 0000000..efc3e98
--- /dev/null
@@ -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<ChangeAdminPasswordApp['props']> = {}) {
+  return shallow<ChangeAdminPasswordApp>(
+    <ChangeAdminPasswordApp
+      canAdmin={true}
+      instanceUsesDefaultAdminCredentials={true}
+      location={mockLocation()}
+      {...props}
+    />
+  );
+}
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 (file)
index 0000000..4861353
--- /dev/null
@@ -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<ChangeAdminPasswordAppRendererProps> = {}) {
+  return shallow<ChangeAdminPasswordAppRendererProps>(
+    <ChangeAdminPasswordAppRenderer
+      canAdmin={true}
+      canSubmit={true}
+      passwordValue="password"
+      confirmPasswordValue="confirm"
+      onConfirmPasswordChange={jest.fn()}
+      onPasswordChange={jest.fn()}
+      onSubmit={jest.fn()}
+      submitting={false}
+      success={false}
+      location={mockLocation()}
+      {...props}
+    />
+  );
+}
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 (file)
index 0000000..5371a8f
--- /dev/null
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: admin is not using the default password 1`] = `
+<ChangeAdminPasswordAppRenderer
+  canAdmin={true}
+  confirmPasswordValue=""
+  location={
+    Object {
+      "action": "PUSH",
+      "hash": "",
+      "key": "key",
+      "pathname": "/path",
+      "query": Object {},
+      "search": "",
+      "state": Object {},
+    }
+  }
+  onConfirmPasswordChange={[Function]}
+  onPasswordChange={[Function]}
+  onSubmit={[Function]}
+  passwordValue=""
+  submitting={false}
+  success={true}
+/>
+`;
+
+exports[`should render correctly: default 1`] = `
+<ChangeAdminPasswordAppRenderer
+  canAdmin={true}
+  confirmPasswordValue=""
+  location={
+    Object {
+      "action": "PUSH",
+      "hash": "",
+      "key": "key",
+      "pathname": "/path",
+      "query": Object {},
+      "search": "",
+      "state": Object {},
+    }
+  }
+  onConfirmPasswordChange={[Function]}
+  onPasswordChange={[Function]}
+  onSubmit={[Function]}
+  passwordValue=""
+  submitting={false}
+  success={false}
+/>
+`;
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 (file)
index 0000000..5c5895a
--- /dev/null
@@ -0,0 +1,377 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly: access denied 1`] = `<Unauthorized />`;
+
+exports[`should render correctly: cannot submit 1`] = `
+<div
+  className="page-wrapper-simple"
+>
+  <div
+    className="page-simple"
+  >
+    <Connect(GlobalMessages) />
+    <h1
+      className="text-center bg-danger big padded"
+    >
+      users.change_admin_password.instance_is_at_risk
+    </h1>
+    <p
+      className="text-center huge huge-spacer-top"
+    >
+      users.change_admin_password.header
+    </p>
+    <p
+      className="text-center huge-spacer-top huge-spacer-bottom"
+    >
+      users.change_admin_password.description
+    </p>
+    <form
+      className="text-center"
+      onSubmit={[Function]}
+    >
+      <h2
+        className="big-spacer-bottom big"
+      >
+        users.change_admin_password.form.header
+      </h2>
+      <MandatoryFieldsExplanation
+        className="form-field"
+      />
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="user-password"
+        >
+          users.change_admin_password.form.password
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="user-password"
+          name="password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="password"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="confirm-user-password"
+        >
+          users.change_admin_password.form.confirm
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="confirm-user-password"
+          name="confirm-password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="confirm"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <SubmitButton
+          disabled={true}
+        >
+          change_verb
+        </SubmitButton>
+      </div>
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: default 1`] = `
+<div
+  className="page-wrapper-simple"
+>
+  <div
+    className="page-simple"
+  >
+    <Connect(GlobalMessages) />
+    <h1
+      className="text-center bg-danger big padded"
+    >
+      users.change_admin_password.instance_is_at_risk
+    </h1>
+    <p
+      className="text-center huge huge-spacer-top"
+    >
+      users.change_admin_password.header
+    </p>
+    <p
+      className="text-center huge-spacer-top huge-spacer-bottom"
+    >
+      users.change_admin_password.description
+    </p>
+    <form
+      className="text-center"
+      onSubmit={[Function]}
+    >
+      <h2
+        className="big-spacer-bottom big"
+      >
+        users.change_admin_password.form.header
+      </h2>
+      <MandatoryFieldsExplanation
+        className="form-field"
+      />
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="user-password"
+        >
+          users.change_admin_password.form.password
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="user-password"
+          name="password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="password"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="confirm-user-password"
+        >
+          users.change_admin_password.form.confirm
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="confirm-user-password"
+          name="confirm-password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="confirm"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <SubmitButton
+          disabled={false}
+        >
+          change_verb
+        </SubmitButton>
+      </div>
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: submitting 1`] = `
+<div
+  className="page-wrapper-simple"
+>
+  <div
+    className="page-simple"
+  >
+    <Connect(GlobalMessages) />
+    <h1
+      className="text-center bg-danger big padded"
+    >
+      users.change_admin_password.instance_is_at_risk
+    </h1>
+    <p
+      className="text-center huge huge-spacer-top"
+    >
+      users.change_admin_password.header
+    </p>
+    <p
+      className="text-center huge-spacer-top huge-spacer-bottom"
+    >
+      users.change_admin_password.description
+    </p>
+    <form
+      className="text-center"
+      onSubmit={[Function]}
+    >
+      <h2
+        className="big-spacer-bottom big"
+      >
+        users.change_admin_password.form.header
+      </h2>
+      <MandatoryFieldsExplanation
+        className="form-field"
+      />
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="user-password"
+        >
+          users.change_admin_password.form.password
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="user-password"
+          name="password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="password"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="confirm-user-password"
+        >
+          users.change_admin_password.form.confirm
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="confirm-user-password"
+          name="confirm-password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="confirm"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <SubmitButton
+          disabled={true}
+        >
+          change_verb
+          <i
+            className="spinner spacer-left"
+          />
+        </SubmitButton>
+      </div>
+    </form>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: success 1`] = `
+<div
+  className="page-wrapper-simple"
+>
+  <div
+    className="page-simple"
+  >
+    <Alert
+      variant="success"
+    >
+      <p
+        className="spacer-bottom"
+      >
+        users.change_admin_password.form.success
+      </p>
+      <a
+        href="/"
+      >
+        users.change_admin_password.form.continue_to_app
+      </a>
+    </Alert>
+  </div>
+</div>
+`;
+
+exports[`should render correctly: trying to use default admin password 1`] = `
+<div
+  className="page-wrapper-simple"
+>
+  <div
+    className="page-simple"
+  >
+    <Connect(GlobalMessages) />
+    <h1
+      className="text-center bg-danger big padded"
+    >
+      users.change_admin_password.instance_is_at_risk
+    </h1>
+    <p
+      className="text-center huge huge-spacer-top"
+    >
+      users.change_admin_password.header
+    </p>
+    <p
+      className="text-center huge-spacer-top huge-spacer-bottom"
+    >
+      users.change_admin_password.description
+    </p>
+    <form
+      className="text-center"
+      onSubmit={[Function]}
+    >
+      <h2
+        className="big-spacer-bottom big"
+      >
+        users.change_admin_password.form.header
+      </h2>
+      <MandatoryFieldsExplanation
+        className="form-field"
+      />
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="user-password"
+        >
+          users.change_admin_password.form.password
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="user-password"
+          name="password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="admin"
+        />
+      </div>
+      <div
+        className="form-field"
+      >
+        <label
+          htmlFor="confirm-user-password"
+        >
+          users.change_admin_password.form.confirm
+          <MandatoryFieldMarker />
+        </label>
+        <input
+          id="confirm-user-password"
+          name="confirm-password"
+          onChange={[Function]}
+          required={true}
+          type="password"
+          value="admin"
+        />
+        <Alert
+          className="spacer-top"
+          variant="warning"
+        >
+          users.change_admin_password.form.cannot_use_default_password
+        </Alert>
+      </div>
+      <div
+        className="form-field"
+      >
+        <SubmitButton
+          disabled={false}
+        >
+          change_verb
+        </SubmitButton>
+      </div>
+    </form>
+  </div>
+</div>
+`;
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 (file)
index 0000000..eecbed8
--- /dev/null
@@ -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';
index 223967cd14e48a57f2aadecc50e76e4bdc23e77a..404c070c28fa178b269492b275129c79366b5615 100644 (file)
@@ -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;
index 2b86440b6d3435e4eb1ebde2fcfceb0ce1d34e35..35e787a8e898409b3772da5134aded1b28f68f40 100644 (file)
@@ -3854,7 +3854,15 @@ users.generate_tokens=Generate Tokens
 users.enter_token_name=Enter Token Name
 users.tokens.new_token_created=New token "{0}" has been created. Make sure you copy it now, you won't be able to see it again!
 users.generate_new_token=Generate New Token
-
+users.change_admin_password.instance_is_at_risk=Secure your SonarQube instance
+users.change_admin_password.header=Default Administrator credentials are still used
+users.change_admin_password.description=Your SonarQube instance is still using default administrator credentials. You must change the password for the 'admin' account to secure your SonarQube instance.
+users.change_admin_password.form.header=Change the password for user 'admin'
+users.change_admin_password.form.password=New password for user 'admin'
+users.change_admin_password.form.confirm=Confirm password for user 'admin'
+users.change_admin_password.form.cannot_use_default_password=You must choose a password that is different from the default password.
+users.change_admin_password.form.success=The admin user's password was successfully changed.
+users.change_admin_password.form.continue_to_app=Continue to SonarQube
 
 #------------------------------------------------------------------------------
 #