]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22938 Admin password complexity first login
authorSarath Nair <sarath.nair@sonarsource.com>
Fri, 6 Sep 2024 15:10:41 +0000 (17:10 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 11 Sep 2024 20:03:48 +0000 (20:03 +0000)
Fix ITs

fix its

Fix ITs

reafctor

Fix test

Fix tests

remove unnecessary check

review comment

server/sonar-web/src/main/js/app/components/__tests__/ResetPassword-test.tsx
server/sonar-web/src/main/js/apps/account/__tests__/Account-it.tsx
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordApp.tsx
server/sonar-web/src/main/js/apps/change-admin-password/ChangeAdminPasswordAppRenderer.tsx
server/sonar-web/src/main/js/apps/change-admin-password/__tests__/ChangeAdminPasswordApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx
server/sonar-web/src/main/js/apps/users/components/UserForm.tsx
server/sonar-web/src/main/js/components/common/ResetPasswordForm.tsx
server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 2faa1fddcd45407ec0a4dc116a9c869f98659bd1..622c16562a7aaa821a3284c8614022f00bc2a19d 100644 (file)
@@ -19,7 +19,7 @@
  */
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import { byLabelText, byRole, byTestId } from '~sonar-aligned/helpers/testSelector';
+import { byLabelText, byRole } from '~sonar-aligned/helpers/testSelector';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderComponent } from '../../../helpers/testReactTestingUtils';
 import { ResetPassword, ResetPasswordProps } from '../ResetPassword';
@@ -76,7 +76,7 @@ function renderResetPassword(props: Partial<ResetPasswordProps> = {}) {
 
 const ui = {
   oldPasswordInput: byLabelText(/my_profile\.password\.old/),
-  passwordInput: byTestId('create-password'),
+  passwordInput: byLabelText(/^password/),
   passwordConfirmationInput: byLabelText(/confirm_password\*/i),
   submitButton: byRole('button', { name: 'update_verb' }),
 };
index e42af7f1c2fb400627e00feafb15921e0c452a53..e8d66831c52e8274f2a2b8b1411f6de1d9be0d6a 100644 (file)
@@ -451,7 +451,7 @@ describe('security page', () => {
       exact: false,
     });
 
-    const newPasswordField = screen.getByTestId('create-password');
+    const newPasswordField = screen.getByLabelText(/^password/);
     const confirmPasswordField = screen.getByLabelText(/confirm_password*/i);
 
     await fillTextField(user, oldPasswordField, '123456old');
index 84226852821240663d8d2759845645bc1cbd1ded..8a8e0b6ee6b027865129ef125b9790becaa3af4c 100644 (file)
@@ -24,7 +24,7 @@ import { changePassword } from '../../api/users';
 import withAppStateContext from '../../app/components/app-state/withAppStateContext';
 import { AppState } from '../../types/appstate';
 import ChangeAdminPasswordAppRenderer from './ChangeAdminPasswordAppRenderer';
-import { DEFAULT_ADMIN_LOGIN, DEFAULT_ADMIN_PASSWORD } from './constants';
+import { DEFAULT_ADMIN_LOGIN } from './constants';
 
 interface Props {
   appState: AppState;
@@ -32,9 +32,6 @@ interface Props {
 }
 
 interface State {
-  canSubmit?: boolean;
-  confirmPasswordValue: string;
-  passwordValue: string;
   submitting: boolean;
   success: boolean;
 }
@@ -46,8 +43,6 @@ export class ChangeAdminPasswordApp extends React.PureComponent<Props, State> {
     super(props);
 
     this.state = {
-      passwordValue: '',
-      confirmPasswordValue: '',
       submitting: false,
       success: !props.appState.instanceUsesDefaultAdminCredentials,
     };
@@ -61,35 +56,21 @@ export class ChangeAdminPasswordApp extends React.PureComponent<Props, State> {
     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({
+  handleSubmit = async (password: string) => {
+    this.setState({ submitting: true });
+    let success = true;
+    try {
+      await changePassword({
         login: DEFAULT_ADMIN_LOGIN,
-        password: passwordValue,
-      }).then(
-        () => true,
-        () => false,
-      );
-      if (this.mounted) {
-        this.setState({ submitting: false, success });
-      }
+        password,
+      });
+    } catch (_) {
+      success = false;
     }
-  };
 
-  checkCanSubmit = () => {
-    this.setState(({ passwordValue, confirmPasswordValue }) => ({
-      canSubmit: passwordValue === confirmPasswordValue && passwordValue !== DEFAULT_ADMIN_PASSWORD,
-    }));
+    if (this.mounted) {
+      this.setState({ submitting: false, success });
+    }
   };
 
   render() {
@@ -97,15 +78,10 @@ export class ChangeAdminPasswordApp extends React.PureComponent<Props, State> {
       appState: { canAdmin },
       location,
     } = this.props;
-    const { canSubmit, confirmPasswordValue, passwordValue, submitting, success } = this.state;
+    const { 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}
index 1ce5f38e4ec95f3da6e7f7fa2ddfdd8a254b8461..97019251bdd3d98370d904f08c38534ea6972bf2 100644 (file)
@@ -24,8 +24,6 @@ import {
   CenteredLayout,
   DarkLabel,
   FlagMessage,
-  FormField,
-  InputField,
   PageContentFontWrapper,
   Spinner,
   SubTitle,
@@ -34,6 +32,9 @@ import {
 import * as React from 'react';
 import { Helmet } from 'react-helmet-async';
 import { Location } from '~sonar-aligned/types/router';
+import UserPasswordInput, {
+  PasswordChangeHandlerParams,
+} from '../../components/common/UserPasswordInput';
 import { translate } from '../../helpers/l10n';
 import { getReturnUrl } from '../../helpers/urls';
 import Unauthorized from '../sessions/components/Unauthorized';
@@ -41,32 +42,21 @@ import { DEFAULT_ADMIN_PASSWORD } from './constants';
 
 export interface ChangeAdminPasswordAppRendererProps {
   canAdmin?: boolean;
-  canSubmit?: boolean;
-  confirmPasswordValue: string;
   location: Location;
-  onConfirmPasswordChange: (password: string) => void;
-  onPasswordChange: (password: string) => void;
-  onSubmit: () => void;
-  passwordValue: string;
+  onSubmit: (password: string) => void;
   submitting: boolean;
   success: boolean;
 }
 
-const PASSWORD_FIELD_ID = 'user-password';
-const CONFIRM_PASSWORD_FIELD_ID = 'confirm-user-password';
-
 export default function ChangeAdminPasswordAppRenderer(
   props: Readonly<ChangeAdminPasswordAppRendererProps>,
 ) {
-  const {
-    canAdmin,
-    canSubmit,
-    confirmPasswordValue,
-    location,
-    passwordValue,
-    submitting,
-    success,
-  } = props;
+  const { canAdmin, location, onSubmit, submitting, success } = props;
+  const [newPassword, setNewPassword] = React.useState<PasswordChangeHandlerParams>({
+    value: '',
+    isValid: false,
+  });
+  const canSubmit = newPassword.isValid && newPassword.value !== DEFAULT_ADMIN_PASSWORD;
 
   if (!canAdmin) {
     return <Unauthorized />;
@@ -103,54 +93,18 @@ export default function ChangeAdminPasswordAppRenderer(
                 className="sw-mt-8"
                 onSubmit={(e: React.SyntheticEvent<HTMLFormElement>) => {
                   e.preventDefault();
-                  props.onSubmit();
+                  onSubmit(newPassword.value);
                 }}
               >
                 <SubTitle className="sw-mb-4">
                   {translate('users.change_admin_password.form.header')}
                 </SubTitle>
 
-                <FormField
-                  htmlFor={PASSWORD_FIELD_ID}
-                  label={translate('users.change_admin_password.form.password')}
-                  required
-                >
-                  <InputField
-                    id={PASSWORD_FIELD_ID}
-                    name="password"
-                    onChange={(e: React.SyntheticEvent<HTMLInputElement>) => {
-                      props.onPasswordChange(e.currentTarget.value);
-                    }}
-                    required
-                    type="password"
-                    value={passwordValue}
-                  />
-                </FormField>
-
-                <FormField
-                  description={
-                    confirmPasswordValue === passwordValue &&
-                    passwordValue === DEFAULT_ADMIN_PASSWORD && (
-                      <FlagMessage className="sw-mt-2" variant="warning">
-                        {translate('users.change_admin_password.form.cannot_use_default_password')}
-                      </FlagMessage>
-                    )
-                  }
-                  htmlFor={CONFIRM_PASSWORD_FIELD_ID}
-                  label={translate('users.change_admin_password.form.confirm')}
-                  required
-                >
-                  <InputField
-                    id={CONFIRM_PASSWORD_FIELD_ID}
-                    name="confirm-password"
-                    onChange={(e: React.SyntheticEvent<HTMLInputElement>) => {
-                      props.onConfirmPasswordChange(e.currentTarget.value);
-                    }}
-                    required
-                    type="password"
-                    value={confirmPasswordValue}
-                  />
-                </FormField>
+                <UserPasswordInput
+                  value={newPassword.value}
+                  onChange={setNewPassword}
+                  size="medium"
+                />
 
                 <ButtonPrimary
                   className="sw-mt-8"
index 5b1c966e92f24e84223ff04e51334e66d193825a..a8c860133895ba7e4622752ca66906915838a072 100644 (file)
@@ -33,18 +33,9 @@ jest.mock('../../../api/users', () => ({
 
 const ui = {
   updateButton: byRole('button', { name: 'update_verb' }),
-  passwordInput: byLabelText('users.change_admin_password.form.password', {
-    selector: 'input',
-    exact: false,
-  }),
-  confirmInput: byLabelText('users.change_admin_password.form.confirm', {
-    selector: 'input',
-    exact: false,
-  }),
+  passwordInput: byLabelText(/^password/),
+  confirmInput: byLabelText(/confirm_password\*/i),
   unauthorizedMessage: byText('unauthorized.message'),
-  defaultPasswordWarningMessage: byText(
-    'users.change_admin_password.form.cannot_use_default_password',
-  ),
 };
 
 it('should disallow change when not an admin', () => {
@@ -58,17 +49,17 @@ it('should allow changing password when using the default admin password', async
     mockAppState({ instanceUsesDefaultAdminCredentials: true, canAdmin: true }),
   );
   expect(ui.updateButton.get()).toBeDisabled();
-  await user.type(ui.passwordInput.get(), 'password');
+  await user.type(ui.passwordInput.get(), 'passworD$123');
 
   expect(ui.updateButton.get()).toBeDisabled();
   await user.type(ui.confirmInput.get(), 'pass');
   expect(ui.updateButton.get()).toBeDisabled();
-  await user.keyboard('word');
+  await user.keyboard('worD$123');
   expect(ui.updateButton.get()).toBeEnabled();
   await user.click(ui.updateButton.get());
   expect(changePassword).toHaveBeenCalledWith({
     login: 'admin',
-    password: 'password',
+    password: 'passworD$123',
   });
 });
 
@@ -82,7 +73,6 @@ it('should not allow to submit the default password', async () => {
   await user.type(ui.confirmInput.get(), DEFAULT_ADMIN_PASSWORD);
 
   expect(ui.updateButton.get()).toBeDisabled();
-  expect(ui.defaultPasswordWarningMessage.get()).toBeInTheDocument();
 });
 
 function renderChangeAdminPasswordApp(appState?: AppState) {
index f01a3cd79f628f5523c9fc363fc562d7300abc1a..b68d476967b77a5b1eceabeab7d5b663bf2a7854 100644 (file)
@@ -23,7 +23,9 @@ import { FlagMessage, FormField, InputField, Modal, addGlobalSuccessMessage } fr
 import * as React from 'react';
 import { changePassword } from '../../../api/users';
 import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
-import UserPasswordInput from '../../../components/common/UserPasswordInput';
+import UserPasswordInput, {
+  PasswordChangeHandlerParams,
+} from '../../../components/common/UserPasswordInput';
 import { translate } from '../../../helpers/l10n';
 import { ChangePasswordResults, RestUserDetailed, isLoggedIn } from '../../../types/users';
 
@@ -40,7 +42,7 @@ export default function PasswordForm(props: Readonly<Props>) {
   const [errorTranslationKey, setErrorTranslationKey] = React.useState<string | undefined>(
     undefined,
   );
-  const [newPassword, setNewPassword] = React.useState<{ isValid: boolean; value: string }>({
+  const [newPassword, setNewPassword] = React.useState<PasswordChangeHandlerParams>({
     value: '',
     isValid: false,
   });
index ae546371e94eca3509594319c7de930f7c170154..116c79a9e6a8a45e38cf698d53583c6c5d5edcf9 100644 (file)
@@ -30,7 +30,9 @@ import {
   addGlobalErrorMessage,
 } from 'design-system';
 import * as React from 'react';
-import UserPasswordInput from '../../../components/common/UserPasswordInput';
+import UserPasswordInput, {
+  PasswordChangeHandlerParams,
+} from '../../../components/common/UserPasswordInput';
 import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { parseErrorResponse } from '../../../helpers/request';
@@ -56,7 +58,7 @@ export default function UserForm(props: Props) {
   const [email, setEmail] = React.useState<string>(user?.email ?? '');
   const [login, setLogin] = React.useState<string>(user?.login ?? '');
   const [name, setName] = React.useState<string>(user?.name ?? '');
-  const [password, setPassword] = React.useState<{ isValid: boolean; value: string }>({
+  const [password, setPassword] = React.useState<PasswordChangeHandlerParams>({
     value: '',
     isValid: false,
   });
index a9d3d4559a7d5fe1f797158a9cc752cc971b61a5..d7cdc2571d15a8764e65f89716a7dd3e436524a3 100644 (file)
@@ -25,7 +25,7 @@ import { changePassword } from '../../api/users';
 import MandatoryFieldsExplanation from '../../components/ui/MandatoryFieldsExplanation';
 import { translate } from '../../helpers/l10n';
 import { ChangePasswordResults, LoggedInUser } from '../../types/users';
-import UserPasswordInput from './UserPasswordInput';
+import UserPasswordInput, { PasswordChangeHandlerParams } from './UserPasswordInput';
 
 interface Props {
   className?: string;
@@ -40,7 +40,7 @@ export default function ResetPasswordForm({
 }: Readonly<Props>) {
   const [error, setError] = React.useState<string | undefined>(undefined);
   const [oldPassword, setOldPassword] = React.useState('');
-  const [password, setPassword] = React.useState<{ isValid: boolean; value: string }>({
+  const [password, setPassword] = React.useState<PasswordChangeHandlerParams>({
     value: '',
     isValid: false,
   });
@@ -111,7 +111,7 @@ export default function ResetPasswordForm({
 
       <div className="sw-py-3">
         <Button
-          isDisabled={oldPassword === '' || password.value === '' || !password.isValid}
+          isDisabled={oldPassword === '' || !password.isValid}
           id="change-password"
           type="submit"
           variety={ButtonVariety.Primary}
index 7336b7a440d7c8f518bd26c0c09a45079f3dfe74..1bced367dcd934c22796518d84677c6749b5a7fc 100644 (file)
@@ -35,10 +35,10 @@ import FocusOutHandler from '../controls/FocusOutHandler';
 
 const MIN_PASSWORD_LENGTH = 12;
 
-export type PasswordChangeHandler = (password: { isValid: boolean; value: string }) => void;
+export type PasswordChangeHandlerParams = { isValid: boolean; value: string };
 
 export interface Props {
-  onChange: PasswordChangeHandler;
+  onChange: (password: PasswordChangeHandlerParams) => void;
   size?: InputSizeKeys;
   value: string;
 }
index 0d2e074e2bff28cf5edb95c0981b4a67d96db212..3977ed8227a1fc1f03cd22f28010e56a248f1386 100644 (file)
@@ -2840,8 +2840,6 @@ my_profile.scm_accounts=SCM Accounts
 my_profile.scm_accounts.tooltip=SCM accounts are used for automatic issue assignment. Login and email are automatically considered as SCM account.
 my_profile.password.title=Enter a new password
 my_profile.password.old=Old Password
-my_profile.password.new=New Password
-my_profile.password.confirm=Confirm Password
 my_profile.password.changed=The password has been changed!
 my_profile.notifications.submit=Save changes
 my_profile.overall_notifications.title=Overall notifications
@@ -5463,9 +5461,6 @@ 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
 users.filter.by=Filter by