]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22936 Update user password complexity (#11694)
authorSarath Nair <91882341+sarath-nair-sonarsource@users.noreply.github.com>
Fri, 6 Sep 2024 10:10:34 +0000 (12:10 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 11 Sep 2024 20:03:48 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx
server/sonar-web/src/main/js/apps/users/components/PasswordForm.tsx
server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 58dd60c1e498514816d12635f7d6f7af37b62ce3..d6d7940c7df4501bbc364a6f1fb1287fd18e491d 100644 (file)
@@ -137,14 +137,12 @@ const ui = {
   loginInput: byRole('textbox', { name: /login/ }),
   userNameInput: byRole('textbox', { name: /name/ }),
   emailInput: byRole('textbox', { name: /email/ }),
-  passwordInput: byLabelText(/password/),
-  confirmPasswordInput: byLabelText(/confirm_password/),
+  passwordInput: byLabelText(/^password/),
   dialogSCMInputs: byRole('textbox', { name: /users.create_user.scm_account/ }),
   dialogSCMInput: (value?: string) =>
     byRole('textbox', { name: `users.create_user.scm_account_${value ? `x.${value}` : 'new'}` }),
   oldPassword: byLabelText('my_profile.password.old', { selector: 'input', exact: false }),
-  newPassword: byLabelText('my_profile.password.new', { selector: 'input', exact: false }),
-  confirmPassword: byLabelText('my_profile.password.confirm', { selector: 'input', exact: false }),
+  confirmPassword: byLabelText(/confirm_password\*/i),
   tokenNameInput: byRole('textbox', { name: 'users.tokens.name' }),
   deleteUserCheckbox: byRole('checkbox', { name: 'users.delete_user' }),
   githubProvisioningPending: byText(/synchronization_pending/),
@@ -265,8 +263,8 @@ describe('in non managed mode', () => {
 
     await user.type(ui.loginInput.get(), 'Login');
     await user.type(ui.userNameInput.get(), 'Jack');
-    await user.type(ui.passwordInput.getAll()[0], 'P@ssword12345');
-    await user.type(ui.passwordInput.getAll()[1], 'P@ssword12345');
+    await user.type(ui.passwordInput.get(), 'P@ssword12345');
+    await user.type(ui.confirmPassword.get(), 'P@ssword12345');
     // Add SCM account
     expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0);
     await user.click(ui.scmAddButton.get());
@@ -421,10 +419,19 @@ describe('in non managed mode', () => {
 
     expect(ui.changeButton.get()).toBeDisabled();
 
-    await user.type(ui.oldPassword.get(), '123');
-    await user.type(ui.newPassword.get(), '1234');
-    await user.type(ui.confirmPassword.get(), '1234');
+    // changes password
+    await user.type(ui.oldPassword.get(), 'test');
+    await user.type(ui.passwordInput.get(), 'AveryStrongP@55');
+    await user.type(ui.confirmPassword.get(), 'AveryStrongP@55');
+    await user.click(ui.changeButton.get());
+    expect(ui.dialogPasswords.query()).not.toBeInTheDocument();
 
+    // cannot change password since old password is wrong
+    await user.click(await ui.aliceUpdateButton.find());
+    await user.click(await byText('my_profile.password.title').find());
+    await user.type(ui.oldPassword.get(), 'test');
+    await user.type(ui.passwordInput.get(), 'AveryStrongP@556');
+    await user.type(ui.confirmPassword.get(), 'AveryStrongP@556');
     expect(ui.changeButton.get()).toBeEnabled();
     expect(
       screen.queryByText(`user.${ChangePasswordResults.OldPasswordIncorrect}`),
@@ -434,12 +441,13 @@ describe('in non managed mode', () => {
       await ui.dialogPasswords.byText(`user.${ChangePasswordResults.OldPasswordIncorrect}`).find(),
     ).toBeInTheDocument();
 
+    // cannot change password since new and old password is same
     await user.clear(ui.oldPassword.get());
-    await user.clear(ui.newPassword.get());
+    await user.clear(ui.passwordInput.get());
     await user.clear(ui.confirmPassword.get());
-    await user.type(ui.oldPassword.get(), 'test');
-    await user.type(ui.newPassword.get(), 'test');
-    await user.type(ui.confirmPassword.get(), 'test');
+    await user.type(ui.oldPassword.get(), 'AveryStrongP@55');
+    await user.type(ui.passwordInput.get(), 'AveryStrongP@55');
+    await user.type(ui.confirmPassword.get(), 'AveryStrongP@55');
 
     expect(
       screen.queryByText(`user.${ChangePasswordResults.NewPasswordSameAsOld}`),
@@ -448,15 +456,6 @@ describe('in non managed mode', () => {
     expect(
       await screen.findByText(`user.${ChangePasswordResults.NewPasswordSameAsOld}`),
     ).toBeInTheDocument();
-
-    await user.clear(ui.newPassword.get());
-    await user.clear(ui.confirmPassword.get());
-    await user.type(ui.newPassword.get(), 'test2');
-    await user.type(ui.confirmPassword.get(), 'test2');
-
-    await user.click(ui.changeButton.get());
-
-    expect(ui.dialogPasswords.query()).not.toBeInTheDocument();
   });
 
   it('should not allow to update non-local user', async () => {
index 2dd07421c0be0c15eea34c8c5409110e35d37a8c..f01a3cd79f628f5523c9fc363fc562d7300abc1a 100644 (file)
@@ -23,6 +23,7 @@ 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 { translate } from '../../../helpers/l10n';
 import { ChangePasswordResults, RestUserDetailed, isLoggedIn } from '../../../types/users';
 
@@ -33,15 +34,17 @@ interface Props {
 
 const PASSWORD_FORM_ID = 'user-password-form';
 
-export default function PasswordForm(props: Props) {
+export default function PasswordForm(props: Readonly<Props>) {
   const { user } = props;
-  const [confirmPassword, setConfirmPassword] = React.useState('');
 
   const [errorTranslationKey, setErrorTranslationKey] = React.useState<string | undefined>(
     undefined,
   );
+  const [newPassword, setNewPassword] = React.useState<{ isValid: boolean; value: string }>({
+    value: '',
+    isValid: false,
+  });
 
-  const [newPassword, setNewPassword] = React.useState('');
   const [oldPassword, setOldPassword] = React.useState('');
   const [submitting, setSubmitting] = React.useState(false);
 
@@ -62,12 +65,12 @@ export default function PasswordForm(props: Props) {
   const handleChangePassword = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
 
-    if (newPassword.length > 0 && newPassword === confirmPassword) {
+    if (newPassword.isValid) {
       setSubmitting(true);
 
       changePassword({
         login: user.login,
-        password: newPassword,
+        password: newPassword.value,
         previousPassword: oldPassword,
       }).then(() => {
         addGlobalSuccessMessage(translate('my_profile.password.changed'));
@@ -114,37 +117,7 @@ export default function PasswordForm(props: Props) {
             </FormField>
           )}
 
-          <FormField htmlFor="user-password" label={translate('my_profile.password.new')} required>
-            <InputField
-              autoFocus
-              id="user-password"
-              name="password"
-              onChange={(event) => setNewPassword(event.currentTarget.value)}
-              required
-              type="password"
-              value={newPassword}
-              size="full"
-            />
-            <input className="sw-hidden" aria-hidden name="password-fake" type="password" />
-          </FormField>
-
-          <FormField
-            htmlFor="confirm-user-password"
-            label={translate('my_profile.password.confirm')}
-            required
-          >
-            <InputField
-              autoFocus
-              id="confirm-user-password"
-              name="confirm-password"
-              onChange={(event) => setConfirmPassword(event.currentTarget.value)}
-              required
-              type="password"
-              value={confirmPassword}
-              size="full"
-            />
-            <input className="sw-hidden" aria-hidden name="confirm-password-fake" type="password" />
-          </FormField>
+          <UserPasswordInput onChange={setNewPassword} value={newPassword.value} />
         </form>
       }
       onClose={props.onClose}
@@ -152,7 +125,7 @@ export default function PasswordForm(props: Props) {
       primaryButton={
         <Button
           form={PASSWORD_FORM_ID}
-          isDisabled={submitting || !newPassword || newPassword !== confirmPassword}
+          isDisabled={submitting || !newPassword.isValid}
           type="submit"
           variety={ButtonVariety.Primary}
         >
index 8f43ca1c716af690bf38d4287426ae6ec6f68eef..c89332354e76cf95bdd0c5e248a2780ed21d9963 100644 (file)
@@ -34,8 +34,10 @@ import FocusOutHandler from '../controls/FocusOutHandler';
 
 const MIN_PASSWORD_LENGTH = 12;
 
+export type PasswordChangeHandler = (password: { isValid: boolean; value: string }) => void;
+
 export interface Props {
-  onChange: (password: { isValid: boolean; value: string }) => void;
+  onChange: PasswordChangeHandler;
   value: string;
 }
 
index 052e2a40627983ffbaa867ca97f3ea1551877e63..0d2e074e2bff28cf5edb95c0981b4a67d96db212 100644 (file)
@@ -2812,6 +2812,7 @@ user.old_password_incorrect=Old password is incorrect
 user.new_password_same_as_old=New password must be different from old password
 user.login_or_email_used_as_scm_account=Login and email are automatically considered as SCM accounts
 user.x_deleted={0} (deleted)
+user.confirm_password.no_match=Passwords do not match
 
 login.page=Log in
 login.login_to_sonarqube=Log in to SonarQube