Move password utilities to their own file (#27366)

* Move password utilities to their own file

* Move passwordErrors to utils/password

* Update file path in test

* Actually commit the whole thing
Этот коммит содержится в:
Harrison Healey
2024-06-19 11:42:38 -04:00
коммит произвёл GitHub
родитель cca0db7e32
Коммит 7d06ac8812
15 изменённых файлов: 354 добавлений и 331 удалений

Просмотреть файл

@@ -9,6 +9,7 @@ import type {AdminConfig} from '@mattermost/types/config';
import type {DeepPartial} from '@mattermost/types/utilities';
import Constants from 'utils/constants';
import {passwordErrors} from 'utils/password';
import AdminSettings from './admin_settings';
import type {BaseProps, BaseState} from './admin_settings';
@@ -63,25 +64,6 @@ export const searchableStrings: Array<string|MessageDescriptor|[MessageDescripto
messages.attemptDescription,
];
const passwordErrors = defineMessages({
passwordError: {id: 'user.settings.security.passwordError', defaultMessage: 'Must be {min}-{max} characters long.'},
passwordErrorLowercase: {id: 'user.settings.security.passwordErrorLowercase', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters.'},
passwordErrorLowercaseNumber: {id: 'user.settings.security.passwordErrorLowercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters and numbers.'},
passwordErrorLowercaseNumberSymbol: {id: 'user.settings.security.passwordErrorLowercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters, numbers, and special characters.'},
passwordErrorLowercaseSymbol: {id: 'user.settings.security.passwordErrorLowercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters and special characters.'},
passwordErrorLowercaseUppercase: {id: 'user.settings.security.passwordErrorLowercaseUppercase', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters.'},
passwordErrorLowercaseUppercaseNumber: {id: 'user.settings.security.passwordErrorLowercaseUppercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, and numbers.'},
passwordErrorLowercaseUppercaseNumberSymbol: {id: 'user.settings.security.passwordErrorLowercaseUppercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, numbers, and special characters.'},
passwordErrorLowercaseUppercaseSymbol: {id: 'user.settings.security.passwordErrorLowercaseUppercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, and special characters.'},
passwordErrorNumber: {id: 'user.settings.security.passwordErrorNumber', defaultMessage: 'Must be {min}-{max} characters long and include numbers.'},
passwordErrorNumberSymbol: {id: 'user.settings.security.passwordErrorNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include numbers and special characters.'},
passwordErrorSymbol: {id: 'user.settings.security.passwordErrorSymbol', defaultMessage: 'Must be {min}-{max} characters long and include special characters.'},
passwordErrorUppercase: {id: 'user.settings.security.passwordErrorUppercase', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters.'},
passwordErrorUppercaseNumber: {id: 'user.settings.security.passwordErrorUppercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, and numbers.'},
passwordErrorUppercaseNumberSymbol: {id: 'user.settings.security.passwordErrorUppercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, numbers, and special characters.'},
passwordErrorUppercaseSymbol: {id: 'user.settings.security.passwordErrorUppercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, and special characters.'},
});
function getPasswordErrorsMessage(lowercase?: boolean, uppercase?: boolean, number?: boolean, symbol?: boolean) {
type KeyType = keyof typeof passwordErrors;

Просмотреть файл

@@ -6,21 +6,17 @@ import {bindActionCreators} from 'redux';
import type {Dispatch} from 'redux';
import {updateUserPassword} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPasswordConfig} from 'mattermost-redux/selectors/entities/general';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {getPasswordConfig} from 'utils/utils';
import type {GlobalState} from 'types/store';
import ResetPasswordModal from './reset_password_modal';
function mapStateToProps(state: GlobalState) {
const config = getConfig(state);
return {
currentUserId: getCurrentUserId(state),
passwordConfig: getPasswordConfig(config),
passwordConfig: getPasswordConfig(state),
};
}

Просмотреть файл

@@ -9,7 +9,7 @@ import type {UserProfile} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';
import * as Utils from 'utils/utils';
import {isValidPassword} from 'utils/password';
interface PasswordConfig {
minimumLength: number;
@@ -83,7 +83,7 @@ export default class ResetPasswordModal extends React.PureComponent<Props, State
const password = (this.passwordRef.current as HTMLInputElement).value;
const {valid, error} = Utils.isValidPassword(password, this.props.passwordConfig);
const {valid, error} = isValidPassword(password, this.props.passwordConfig);
if (!valid && error) {
this.setState({serverErrorNewPass: error});
return;

Просмотреть файл

@@ -12,7 +12,8 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import LoginMfa from 'components/login/login_mfa';
import {ClaimErrors} from 'utils/constants';
import {isValidPassword, localizeMessage} from 'utils/utils';
import {isValidPassword} from 'utils/password';
import {localizeMessage} from 'utils/utils';
import type {SubmitOptions} from './email_to_ldap';
import ErrorLabel from './error_label';

Просмотреть файл

@@ -7,11 +7,13 @@ import {FormattedMessage, useIntl} from 'react-intl';
import type {AuthChangeResponse} from '@mattermost/types/users';
import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general';
import {oauthToEmail} from 'actions/admin_actions.jsx';
import Constants from 'utils/constants';
import {isValidPassword, localizeMessage, toTitleCase} from 'utils/utils';
import type {getPasswordConfig} from 'utils/utils';
import {isValidPassword} from 'utils/password';
import {localizeMessage, toTitleCase} from 'utils/utils';
import ErrorLabel from './error_label';
@@ -19,7 +21,7 @@ type Props = {
currentType: string | null;
email: string | null;
siteName?: string;
passwordConfig?: ReturnType<typeof getPasswordConfig>;
passwordConfig?: PasswordConfig;
}
const OAuthToEmail = (props: Props) => {

Просмотреть файл

@@ -8,9 +8,7 @@ import type {Dispatch} from 'redux';
import type {GlobalState} from '@mattermost/types/store';
import {switchLdapToEmail} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getPasswordConfig} from 'utils/utils';
import {getConfig, getPasswordConfig} from 'mattermost-redux/selectors/entities/general';
import ClaimController from './claim_controller';
@@ -22,7 +20,7 @@ function mapStateToProps(state: GlobalState) {
return {
siteName,
ldapLoginFieldName,
passwordConfig: getPasswordConfig(config),
passwordConfig: getPasswordConfig(state),
};
}

Просмотреть файл

@@ -15,7 +15,7 @@ import type {UserProfile} from '@mattermost/types/users';
import {getTeamInviteInfo} from 'mattermost-redux/actions/teams';
import {createUser, loadMe} from 'mattermost-redux/actions/users';
import {Client4} from 'mattermost-redux/client';
import {getConfig, getLicense} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getLicense, getPasswordConfig} from 'mattermost-redux/selectors/entities/general';
import {getIsOnboardingFlowEnabled} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {isEmail} from 'mattermost-redux/utils/helpers';
@@ -54,8 +54,9 @@ import type {CustomMessageInputType} from 'components/widgets/inputs/input/input
import PasswordInput from 'components/widgets/inputs/password_input/password_input';
import {Constants, HostedCustomerLinks, ItemStatus, ValidationErrors} from 'utils/constants';
import {isValidPassword} from 'utils/password';
import {isDesktopApp} from 'utils/user_agent';
import {isValidUsername, isValidPassword, getPasswordConfig, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils';
import {isValidUsername, getRoleFromTrackFlow, getMediumFromTrackFlow} from 'utils/utils';
import type {GlobalState} from 'types/store';
@@ -148,7 +149,8 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
const enableExternalSignup = enableSignUpWithGitLab || enableSignUpWithOffice365 || enableSignUpWithGoogle || enableSignUpWithOpenId || enableLDAP || enableSAML;
const hasError = Boolean(emailError || nameError || passwordError || serverError || alertBanner);
const canSubmit = Boolean(email && name && password) && !hasError && !loading;
const {error: passwordInfo} = isValidPassword('', getPasswordConfig(config), intl);
const passwordConfig = useSelector(getPasswordConfig);
const {error: passwordInfo} = isValidPassword('', passwordConfig, intl);
const [desktopLoginLink, setDesktopLoginLink] = useState('');
@@ -553,7 +555,7 @@ const Signup = ({onCustomizeHeader}: SignupProps) => {
}
const providedPassword = passwordInput.current?.value ?? '';
const {error, telemetryErrorIds} = isValidPassword(providedPassword, getPasswordConfig(config), intl);
const {error, telemetryErrorIds} = isValidPassword(providedPassword, passwordConfig, intl);
if (error) {
setPasswordError(error as string);

Просмотреть файл

@@ -10,12 +10,11 @@ import type {UserProfile} from '@mattermost/types/users';
import {getAuthorizedOAuthApps, deauthorizeOAuthApp} from 'mattermost-redux/actions/integrations';
import {getMe, updateUserPassword} from 'mattermost-redux/actions/users';
import {getConfig} from 'mattermost-redux/selectors/entities/general';
import {getConfig, getPasswordConfig} from 'mattermost-redux/selectors/entities/general';
import {getBool} from 'mattermost-redux/selectors/entities/preferences';
import * as UserUtils from 'mattermost-redux/utils/user_utils';
import {Preferences} from 'utils/constants';
import {getPasswordConfig} from 'utils/utils';
import SecurityTab from './user_settings_security';
@@ -55,7 +54,7 @@ function mapStateToProps(state: GlobalState, ownProps: Props) {
enableSaml,
enableSignUpWithOffice365,
experimentalEnableAuthenticationTransfer,
passwordConfig: getPasswordConfig(config),
passwordConfig: getPasswordConfig(state),
militaryTime: getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false),
};
}

Просмотреть файл

@@ -7,14 +7,15 @@ import React from 'react';
import type {OAuthApp} from '@mattermost/types/integrations';
import type {UserProfile} from '@mattermost/types/users';
import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general';
import type {MockIntl} from 'tests/helpers/intl-test-helper';
import Constants from 'utils/constants';
import type * as Utils from 'utils/utils';
import {SecurityTab} from './user_settings_security';
jest.mock('utils/utils', () => {
const original = jest.requireActual('utils/utils');
jest.mock('utils/password', () => {
const original = jest.requireActual('utils/password');
return {...original, isValidPassword: () => ({valid: true})};
});
@@ -46,7 +47,7 @@ describe('components/user_settings/display/UserSettingsDisplay', () => {
enableSaml: true,
enableSignUpWithOffice365: false,
experimentalEnableAuthenticationTransfer: true,
passwordConfig: {} as ReturnType<typeof Utils.getPasswordConfig>,
passwordConfig: {} as PasswordConfig,
militaryTime: false,
intl: {
formatMessage: jest.fn(({id, defaultMessage}) => defaultMessage || id),

Просмотреть файл

@@ -11,6 +11,7 @@ import {Link} from 'react-router-dom';
import type {OAuthApp} from '@mattermost/types/integrations';
import type {UserProfile} from '@mattermost/types/users';
import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general';
import type {ActionResult} from 'mattermost-redux/types/actions';
import AccessHistoryModal from 'components/access_history_modal';
@@ -22,7 +23,7 @@ import ToggleModalButton from 'components/toggle_modal_button';
import icon50 from 'images/icon50x50.png';
import Constants from 'utils/constants';
import * as Utils from 'utils/utils';
import {isValidPassword} from 'utils/password';
import MfaSection from './mfa_section';
import UserAccessTokenSection from './user_access_token_section';
@@ -64,7 +65,7 @@ type Props = {
enableSaml: boolean;
enableSignUpWithOffice365: boolean;
experimentalEnableAuthenticationTransfer: boolean;
passwordConfig: ReturnType<typeof Utils.getPasswordConfig>;
passwordConfig: PasswordConfig;
militaryTime: boolean;
actions: Actions;
intl: IntlShape;
@@ -135,7 +136,7 @@ export class SecurityTab extends React.PureComponent<Props, State> {
return;
}
const {valid, error} = Utils.isValidPassword(
const {valid, error} = isValidPassword(
newPassword,
this.props.passwordConfig,
);

Просмотреть файл

@@ -21,6 +21,28 @@ export function getFeatureFlagValue(state: GlobalState, key: keyof FeatureFlags)
return getConfig(state)?.[`FeatureFlag${key}` as keyof Partial<ClientConfig>];
}
export type PasswordConfig = {
minimumLength: number;
requireLowercase: boolean;
requireUppercase: boolean;
requireNumber: boolean;
requireSymbol: boolean;
};
export const getPasswordConfig: (state: GlobalState) => PasswordConfig = createSelector(
'getPasswordConfig',
getConfig,
(config) => {
return {
minimumLength: parseInt(config.PasswordMinimumLength!, 10),
requireLowercase: config.PasswordRequireLowercase === 'true',
requireUppercase: config.PasswordRequireUppercase === 'true',
requireNumber: config.PasswordRequireNumber === 'true',
requireSymbol: config.PasswordRequireSymbol === 'true',
};
},
);
export function getLicense(state: GlobalState): ClientLicense {
return state.entities.general.license;
}

197
webapp/channels/src/utils/password.test.tsx Обычный файл
Просмотреть файл

@@ -0,0 +1,197 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {isValidPassword} from './password';
describe('isValidPassword', () => {
test('Minimum length enforced', () => {
for (const data of [
{
password: 'tooshort',
config: {
minimumLength: 10,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
{
password: 'longenoughpassword',
config: {
minimumLength: 10,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Maximum length enforced', () => {
for (const data of [
{
password: 'justright',
config: {
minimumLength: 8,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
{
password: 'iamaverylongstringthathas72charactersandwillpasswithoutanyissuesthiscall',
config: {
minimumLength: 8,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
{
password: 'iamaverylongstringthathas73charactersandwontpassthisvalidationatall!!!:-(',
config: {
minimumLength: 8,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
]) {
const {valid} = isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require lowercase enforced', () => {
for (const data of [
{
password: 'UPPERCASE',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
{
password: 'SOMELowercase',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require uppercase enforced', () => {
for (const data of [
{
password: 'lowercase',
config: {
minimumLength: 5,
requireLowercase: false,
requireUppercase: true,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
{
password: 'SOMEUppercase',
config: {
minimumLength: 5,
requireLowercase: false,
requireUppercase: true,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require number enforced', () => {
for (const data of [
{
password: 'NoNumbers',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: false,
},
valid: false,
},
{
password: 'S0m3Numb3rs',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require symbol enforced', () => {
for (const data of [
{
password: 'N0Symb0ls',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: true,
},
valid: false,
},
{
password: 'S0m3Symb0!s',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: true,
},
valid: true,
},
]) {
const {valid} = isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
});

104
webapp/channels/src/utils/password.tsx Обычный файл
Просмотреть файл

@@ -0,0 +1,104 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import type {IntlShape} from 'react-intl';
import {FormattedMessage, defineMessages} from 'react-intl';
import type {PasswordConfig} from 'mattermost-redux/selectors/entities/general';
import Constants from 'utils/constants';
export function isValidPassword(password: string, passwordConfig: PasswordConfig, intl?: IntlShape) {
let errorId = passwordErrors.passwordError.id;
const telemetryErrorIds = [];
let valid = true;
const minimumLength = passwordConfig.minimumLength || Constants.MIN_PASSWORD_LENGTH;
if (password.length < minimumLength || password.length > Constants.MAX_PASSWORD_LENGTH) {
valid = false;
telemetryErrorIds.push({field: 'password', rule: 'error_length'});
}
if (passwordConfig.requireLowercase) {
if (!password.match(/[a-z]/)) {
valid = false;
}
errorId += 'Lowercase';
telemetryErrorIds.push({field: 'password', rule: 'lowercase'});
}
if (passwordConfig.requireUppercase) {
if (!password.match(/[A-Z]/)) {
valid = false;
}
errorId += 'Uppercase';
telemetryErrorIds.push({field: 'password', rule: 'uppercase'});
}
if (passwordConfig.requireNumber) {
if (!password.match(/[0-9]/)) {
valid = false;
}
errorId += 'Number';
telemetryErrorIds.push({field: 'password', rule: 'number'});
}
if (passwordConfig.requireSymbol) {
if (!password.match(/[ !"\\#$%&'()*+,-./:;<=>?@[\]^_`|~]/)) {
valid = false;
}
errorId += 'Symbol';
telemetryErrorIds.push({field: 'password', rule: 'symbol'});
}
let error;
if (!valid) {
error = intl ? (
intl.formatMessage(
{
id: errorId,
defaultMessage: 'Must be {min}-{max} characters long.',
},
{
min: minimumLength,
max: Constants.MAX_PASSWORD_LENGTH,
},
)
) : (
<FormattedMessage
id={errorId}
defaultMessage='Must be {min}-{max} characters long.'
values={{
min: minimumLength,
max: Constants.MAX_PASSWORD_LENGTH,
}}
/>
);
}
return {valid, error, telemetryErrorIds};
}
export const passwordErrors = defineMessages({
passwordError: {id: 'user.settings.security.passwordError', defaultMessage: 'Must be {min}-{max} characters long.'},
passwordErrorLowercase: {id: 'user.settings.security.passwordErrorLowercase', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters.'},
passwordErrorLowercaseNumber: {id: 'user.settings.security.passwordErrorLowercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters and numbers.'},
passwordErrorLowercaseNumberSymbol: {id: 'user.settings.security.passwordErrorLowercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters, numbers, and special characters.'},
passwordErrorLowercaseSymbol: {id: 'user.settings.security.passwordErrorLowercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include lowercase letters and special characters.'},
passwordErrorLowercaseUppercase: {id: 'user.settings.security.passwordErrorLowercaseUppercase', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters.'},
passwordErrorLowercaseUppercaseNumber: {id: 'user.settings.security.passwordErrorLowercaseUppercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, and numbers.'},
passwordErrorLowercaseUppercaseNumberSymbol: {id: 'user.settings.security.passwordErrorLowercaseUppercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, numbers, and special characters.'},
passwordErrorLowercaseUppercaseSymbol: {id: 'user.settings.security.passwordErrorLowercaseUppercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include both lowercase and uppercase letters, and special characters.'},
passwordErrorNumber: {id: 'user.settings.security.passwordErrorNumber', defaultMessage: 'Must be {min}-{max} characters long and include numbers.'},
passwordErrorNumberSymbol: {id: 'user.settings.security.passwordErrorNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include numbers and special characters.'},
passwordErrorSymbol: {id: 'user.settings.security.passwordErrorSymbol', defaultMessage: 'Must be {min}-{max} characters long and include special characters.'},
passwordErrorUppercase: {id: 'user.settings.security.passwordErrorUppercase', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters.'},
passwordErrorUppercaseNumber: {id: 'user.settings.security.passwordErrorUppercaseNumber', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, and numbers.'},
passwordErrorUppercaseNumberSymbol: {id: 'user.settings.security.passwordErrorUppercaseNumberSymbol', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, numbers, and special characters.'},
passwordErrorUppercaseSymbol: {id: 'user.settings.security.passwordErrorUppercaseSymbol', defaultMessage: 'Must be {min}-{max} characters long and include uppercase letters, and special characters.'},
});

Просмотреть файл

@@ -93,199 +93,6 @@ describe('Utils.getDisplayNameByUser', () => {
});
});
describe('Utils.isValidPassword', () => {
test('Minimum length enforced', () => {
for (const data of [
{
password: 'tooshort',
config: {
minimumLength: 10,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
{
password: 'longenoughpassword',
config: {
minimumLength: 10,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = Utils.isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Maximum length enforced', () => {
for (const data of [
{
password: 'justright',
config: {
minimumLength: 8,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
{
password: 'iamaverylongstringthathas72charactersandwillpasswithoutanyissuesthiscall',
config: {
minimumLength: 8,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
{
password: 'iamaverylongstringthathas73charactersandwontpassthisvalidationatall!!!:-(',
config: {
minimumLength: 8,
requireLowercase: false,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
]) {
const {valid} = Utils.isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require lowercase enforced', () => {
for (const data of [
{
password: 'UPPERCASE',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
{
password: 'SOMELowercase',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: false,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = Utils.isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require uppercase enforced', () => {
for (const data of [
{
password: 'lowercase',
config: {
minimumLength: 5,
requireLowercase: false,
requireUppercase: true,
requireNumber: false,
requireSymbol: false,
},
valid: false,
},
{
password: 'SOMEUppercase',
config: {
minimumLength: 5,
requireLowercase: false,
requireUppercase: true,
requireNumber: false,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = Utils.isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require number enforced', () => {
for (const data of [
{
password: 'NoNumbers',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: false,
},
valid: false,
},
{
password: 'S0m3Numb3rs',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: false,
},
valid: true,
},
]) {
const {valid} = Utils.isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
test('Require symbol enforced', () => {
for (const data of [
{
password: 'N0Symb0ls',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: true,
},
valid: false,
},
{
password: 'S0m3Symb0!s',
config: {
minimumLength: 5,
requireLowercase: true,
requireUppercase: true,
requireNumber: true,
requireSymbol: true,
},
valid: true,
},
]) {
const {valid} = Utils.isValidPassword(data.password, data.config);
expect(data.valid).toEqual(valid);
}
});
});
describe('Utils.isValidUsername', () => {
const tests = [
{

Просмотреть файл

@@ -9,12 +9,9 @@ import isNil from 'lodash/isNil';
import moment from 'moment';
import React from 'react';
import type {LinkHTMLAttributes} from 'react';
import {FormattedMessage} from 'react-intl';
import type {IntlShape} from 'react-intl';
import type {Channel} from '@mattermost/types/channels';
import type {Address} from '@mattermost/types/cloud';
import type {ClientConfig} from '@mattermost/types/config';
import type {FileInfo} from '@mattermost/types/files';
import type {Group} from '@mattermost/types/groups';
import type {GlobalState} from '@mattermost/types/store';
@@ -1224,92 +1221,6 @@ export function mod(a: number, b: number): number {
export const REACTION_PATTERN = /^(\+|-):([^:\s]+):\s*$/;
export function getPasswordConfig(config: Partial<ClientConfig>) {
return {
minimumLength: parseInt(config.PasswordMinimumLength!, 10),
requireLowercase: config.PasswordRequireLowercase === 'true',
requireUppercase: config.PasswordRequireUppercase === 'true',
requireNumber: config.PasswordRequireNumber === 'true',
requireSymbol: config.PasswordRequireSymbol === 'true',
};
}
export function isValidPassword(password: string, passwordConfig: ReturnType<typeof getPasswordConfig>, intl?: IntlShape) {
// The translation strings used by this function are defined in admin_console/password_settings
let errorId = 'user.settings.security.passwordError';
const telemetryErrorIds = [];
let valid = true;
const minimumLength = passwordConfig.minimumLength || Constants.MIN_PASSWORD_LENGTH;
if (password.length < minimumLength || password.length > Constants.MAX_PASSWORD_LENGTH) {
valid = false;
telemetryErrorIds.push({field: 'password', rule: 'error_length'});
}
if (passwordConfig.requireLowercase) {
if (!password.match(/[a-z]/)) {
valid = false;
}
errorId += 'Lowercase';
telemetryErrorIds.push({field: 'password', rule: 'lowercase'});
}
if (passwordConfig.requireUppercase) {
if (!password.match(/[A-Z]/)) {
valid = false;
}
errorId += 'Uppercase';
telemetryErrorIds.push({field: 'password', rule: 'uppercase'});
}
if (passwordConfig.requireNumber) {
if (!password.match(/[0-9]/)) {
valid = false;
}
errorId += 'Number';
telemetryErrorIds.push({field: 'password', rule: 'number'});
}
if (passwordConfig.requireSymbol) {
if (!password.match(/[ !"\\#$%&'()*+,-./:;<=>?@[\]^_`|~]/)) {
valid = false;
}
errorId += 'Symbol';
telemetryErrorIds.push({field: 'password', rule: 'symbol'});
}
let error;
if (!valid) {
error = intl ? (
intl.formatMessage(
{
id: errorId,
defaultMessage: 'Must be {min}-{max} characters long.',
},
{
min: minimumLength,
max: Constants.MAX_PASSWORD_LENGTH,
},
)
) : (
<FormattedMessage
id={errorId}
defaultMessage='Must be {min}-{max} characters long.'
values={{
min: minimumLength,
max: Constants.MAX_PASSWORD_LENGTH,
}}
/>
);
}
return {valid, error, telemetryErrorIds};
}
function isChannelOrPermalink(link: string) {
let match = (/\/([a-z0-9\-_]+)\/channels\/([a-z0-9\-__][a-z0-9\-__.]+)/).exec(link);
if (match) {