From: guillaume-peoch-sonarsource Date: Tue, 3 Sep 2024 13:18:25 +0000 (+0200) Subject: SONAR-22931 Add password complexity when creating users as admin X-Git-Tag: 10.7.0.96327~129 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=bdcd59aefa14abd2456db15b571fbaaeb681218c;p=sonarqube.git SONAR-22931 Add password complexity when creating users as admin Removing zxcvbn Confirm Password Fix CI issues Fix SQ analysis issue --- diff --git a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx index 97c1404bf7c..58dd60c1e49 100644 --- a/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/users/__tests__/UsersApp-it.tsx @@ -138,6 +138,7 @@ const ui = { userNameInput: byRole('textbox', { name: /name/ }), emailInput: byRole('textbox', { name: /email/ }), passwordInput: byLabelText(/password/), + confirmPasswordInput: byLabelText(/confirm_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'}` }), @@ -264,7 +265,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.get(), 'Password'); + await user.type(ui.passwordInput.getAll()[0], 'P@ssword12345'); + await user.type(ui.passwordInput.getAll()[1], 'P@ssword12345'); // Add SCM account expect(ui.dialogSCMInputs.queryAll()).toHaveLength(0); await user.click(ui.scmAddButton.get()); @@ -736,7 +738,7 @@ it('accessibility', async () => { // user creation dialog should be accessible await user.click(await ui.createUserButton.find()); expect(await ui.dialogCreateUser.find()).toBeInTheDocument(); - await expect(ui.dialogCreateUser.get()).toHaveNoA11yViolations(); + await expect(await ui.dialogCreateUser.find()).toHaveNoA11yViolations(); await user.click(ui.cancelButton.get()); diff --git a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx index 408bf512da0..ae546371e94 100644 --- a/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/UserForm.tsx @@ -30,6 +30,7 @@ import { addGlobalErrorMessage, } from 'design-system'; import * as React from 'react'; +import UserPasswordInput from '../../../components/common/UserPasswordInput'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { parseErrorResponse } from '../../../helpers/request'; @@ -55,7 +56,10 @@ export default function UserForm(props: Props) { const [email, setEmail] = React.useState(user?.email ?? ''); const [login, setLogin] = React.useState(user?.login ?? ''); const [name, setName] = React.useState(user?.name ?? ''); - const [password, setPassword] = React.useState(''); + const [password, setPassword] = React.useState<{ isValid: boolean; value: string }>({ + value: '', + isValid: false, + }); const [scmAccounts, setScmAccounts] = React.useState(user?.scmAccounts ?? []); const [error, setError] = React.useState(undefined); @@ -88,7 +92,7 @@ export default function UserForm(props: Props) { email: email || undefined, login, name, - password, + password: password.value, scmAccounts, }, { onSuccess: props.onClose, onError: handleError }, @@ -215,17 +219,10 @@ export default function UserForm(props: Props) { {!user && ( - - setPassword(e.currentTarget.value)} - type="password" - value={password} - /> - + setPassword(password)} + /> )} void; + value: string; +} + +export default function EmailIput(props: Readonly) { + const { id, value, onChange } = props; + + const [isEmailValid, setIsEmailValid] = React.useState(true); + + return ( + setIsEmailValid(isEmail(value))}> + { + onChange({ value: currentTarget.value, isValid: isEmail(currentTarget.value) }); + }} + type="email" + value={value} + /> + {isEmailValid === false && ( + + )} + + ); +} diff --git a/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx b/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx new file mode 100644 index 00000000000..8f43ca1c716 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/UserPasswordInput.tsx @@ -0,0 +1,185 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 styled from '@emotion/styled'; +import { IconCheck, IconX } from '@sonarsource/echoes-react'; +import { + FlagErrorIcon, + FlagSuccessIcon, + FormField, + InputField, + LightLabel, + TextError, + themeColor, +} from 'design-system'; +import * as React from 'react'; +import { translate } from '../../helpers/l10n'; +import FocusOutHandler from '../controls/FocusOutHandler'; + +const MIN_PASSWORD_LENGTH = 12; + +export interface Props { + onChange: (password: { isValid: boolean; value: string }) => void; + value: string; +} + +export default function UserPasswordInput(props: Readonly) { + const { value, onChange } = props; + + const [isFocused, setIsFocused] = React.useState(false); + const [confirmValue, setConfirmValue] = React.useState(''); + + const isInvalid = !isFocused && value !== '' && !isPasswordValid(value); + const isValid = !isFocused && isPasswordValid(value); + const passwordMatch = isPasswordConfirmed(value, confirmValue); + const passwordDontMatch = value !== confirmValue && confirmValue !== ''; + + return ( + <> + setIsFocused(false)}> + +
+ setIsFocused(true)} + id="create-password" + size="full" + onChange={({ currentTarget }) => { + onChange({ + value: currentTarget.value, + isValid: + isPasswordValid(currentTarget.value) && + isPasswordConfirmed(currentTarget.value, confirmValue), + }); + }} + type="password" + value={value} + /> + {isInvalid && } + {isValid && } +
+
+
+ {isInvalid && } + {isFocused && } + + +
+ setIsFocused(true)} + id="confirm-password" + size="full" + onChange={({ currentTarget }) => { + setConfirmValue(currentTarget.value); + onChange({ + value, + isValid: + isPasswordValid(currentTarget.value) && + isPasswordConfirmed(value, currentTarget.value), + }); + }} + type="password" + value={confirmValue} + /> + {passwordDontMatch && } + {passwordMatch && } +
+ {passwordDontMatch && ( + + )} +
+ + ); +} + +function PasswordConstraint({ value }: Readonly<{ value: string }>) { + return ( +
+ {translate('user.password.conditions')} +
    + + + + + +
+
+ ); +} + +function Condition({ condition, label }: Readonly<{ condition: boolean; label: string }>) { + return ( +
  • + {condition ? ( + + + {label} + + ) : ( + + + {label} + + )} +
  • + ); +} + +const contains12Characters = (password: string) => password.length >= MIN_PASSWORD_LENGTH; +const containsUppercase = (password: string) => /[A-Z]/.test(password); +const containsLowercase = (password: string) => /[a-z]/.test(password); +const containsDigit = (password: string) => /\d/.test(password); +const containsSpecialCharacter = (password: string) => /[^a-zA-Z0-9]/.test(password); + +const isPasswordValid = (password: string) => + contains12Characters(password) && + containsUppercase(password) && + containsLowercase(password) && + containsDigit(password) && + containsSpecialCharacter(password); + +const isPasswordConfirmed = (password: string, confirm: string) => + password === confirm && password !== ''; + +const SuccessLabel = styled(LightLabel)` + color: ${themeColor('textSuccess')}; +`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 13095e0a393..052e2a40627 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -172,6 +172,7 @@ order=Order owner=Owner parameters=Parameters password=Password +confirm_password=Confirm Password path=Path permalink=Permanent Link plugin=Plugin @@ -2799,6 +2800,14 @@ alert.dismiss=Dismiss this message # #------------------------------------------------------------------------------ user.password_doesnt_match_confirmation=Password doesn't match confirmation. +user.password.conditions=Your password must include at least: +user.password.condition.12_characters=At least 12 characters +user.password.condition.1_upper_case=At least one uppercase letter +user.password.condition.1_lower_case=At least one lowercase letter +user.password.condition.1_number=At least one digit +user.password.condition.1_special_character=At least one special character +user.password.invalid=Your password must meet all the requirements. +user.password.do_not_match=Passwords do not match. 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