/* * Copyright (C) 2023 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import { debounce } from "throttle-debounce"; import React, { useEffect, useMemo, useState } from "react"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { FormGroup, FormHelperText, FormSection } from "@patternfly/react-core/dist/esm/components/Form/index.js"; import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js"; import { InputGroup, InputGroupItem } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js"; import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js"; import { CheckCircleIcon } from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; import { ExclamationCircleIcon } from "@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon"; import { ExclamationTriangleIcon } from "@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon"; import { EyeIcon } from "@patternfly/react-icons/dist/esm/icons/eye-icon"; import { EyeSlashIcon } from "@patternfly/react-icons/dist/esm/icons/eye-slash-icon"; // eslint-disable-next-line camelcase import { password_quality } from "cockpit-components-password.jsx"; const _ = cockpit.gettext; export const ruleLength = { check: (policy, password) => password.length >= policy["min-length"].v, id: "length", isError: true, text: (policy) => cockpit.format(_("Must be at least $0 characters"), policy["min-length"].v), }; /* Calculate the password quality levels based on the password policy * If the policy specifies a 'is-strict' rule anything bellow the minimum specified by the policy * is considered invalid * @param {int} minQualility - the minimum quality level * @return {array} - the password strengh levels */ const getStrengthLevels = (minQualility, isStrict) => { const levels = [{ higher_bound: minQualility - 1, icon: , id: "weak", label: _("Weak"), lower_bound: 0, valid: !isStrict, variant: "error", }]; if (minQualility <= 69) { levels.push({ higher_bound: 69, icon: , id: "medium", label: _("Medium"), lower_bound: minQualility, valid: true, variant: "warning", }); } levels.push({ higher_bound: 100, icon: , id: "strong", label: _("Strong"), lower_bound: Math.max(70, minQualility), valid: true, variant: "success", }); return levels; }; const getRuleResults = (rules, policy, password) => { return rules.map(rule => { return { id: rule.id, isError: rule.isError, isSatisfied: password.length > 0 ? rule.check(policy, password) : null, text: rule.text(policy, password) }; }); }; const rulesSatisfied = ruleResults => ruleResults.every(r => r.isSatisfied || !r.isError); const passwordStrengthLabel = (idPrefix, strength, strengthLevels) => { const level = strengthLevels.filter(l => l.id === strength)[0]; if (level) { return ( {level.label} ); } }; export const PasswordFormFields = ({ confirmPassword, confirmPasswordLabel, emptyPasswordHelperText, idPrefix, password, passwordLabel, passwordRef, policy, rules, sectionDescription, setConfirmPassword, setIsValid, setPassword, }) => { const [passwordHidden, setPasswordHidden] = useState(true); const [confirmHidden, setConfirmHidden] = useState(true); const [checkPassword, setCheckPassword] = useState(password); const [checkConfirmPassword, setCheckConfirmPassword] = useState(confirmPassword); const [passwordStrength, setPasswordStrength] = useState(""); useEffect(() => { debounce(300, () => setCheckPassword(password))(); }, [password, setCheckPassword]); useEffect(() => { debounce(300, () => setCheckConfirmPassword(confirmPassword))(); }, [confirmPassword, setCheckConfirmPassword]); const ruleResults = useMemo(() => { return getRuleResults(rules, policy, checkPassword); }, [policy, checkPassword, rules]); const allowEmptyPassword = policy?.["allow-empty"]?.v ?? false; const bothFieldsEmpty = (checkPassword?.length ?? 0) === 0 && (checkConfirmPassword?.length ?? 0) === 0; const isEmptyPasswordAccepted = allowEmptyPassword && bothFieldsEmpty; const ruleConfirmMatches = bothFieldsEmpty ? (allowEmptyPassword ? true : null) : checkPassword === checkConfirmPassword; const ruleHelperItems = ruleResults.map(rule => { let variant = rule.isSatisfied === null ? "indeterminate" : rule.isSatisfied ? "success" : "error"; if (!rule.isError) { if (rule.isSatisfied || rule.isSatisfied === null) { return null; } variant = "warning"; } return ( {rule.text} ); }); const ruleConfirmVariant = ruleConfirmMatches === null ? "indeterminate" : ruleConfirmMatches ? "success" : "error"; const strengthLevels = useMemo(() => { return policy && getStrengthLevels(policy["min-quality"].v, policy["is-strict"].v); }, [policy]); useEffect(() => { const updatePasswordStrength = async () => { const _passwordStrength = await getPasswordStrength(checkPassword, strengthLevels); setPasswordStrength(_passwordStrength); }; updatePasswordStrength(); }, [checkPassword, strengthLevels]); useEffect(() => { const isStrict = policy?.["is-strict"]?.v ?? false; if (isEmptyPasswordAccepted) { setIsValid(true); return; } if (!ruleConfirmMatches) { setIsValid(false); return; } if (isStrict) { setIsValid(!!(rulesSatisfied(ruleResults) && isValidStrength(passwordStrength, strengthLevels))); return; } // Non-strict: length/strength are shown but not required; only confirmation must match. setIsValid((checkPassword?.length ?? 0) > 0); }, [ checkPassword, isEmptyPasswordAccepted, policy, ruleConfirmMatches, ruleResults, passwordStrength, setIsValid, strengthLevels, ]); return ( {sectionDescription} setPassword(val)} id={idPrefix + "-password-field"} ref={passwordRef} /> {ruleHelperItems} {allowEmptyPassword && emptyPasswordHelperText && ( {emptyPasswordHelperText} )} setConfirmPassword(val)} id={idPrefix + "-password-confirm-field"} /> {_("Passphrases must match")} ); }; const getPasswordStrength = async (password, strengthLevels) => { // In case of unacceptable password just return 0 const force = true; const quality = await password_quality(password, force); const level = strengthLevels.filter(l => l.lower_bound <= quality.value && l.higher_bound >= quality.value)[0]; return level.id; }; const isValidStrength = (strength, strengthLevels) => { const level = strengthLevels.filter(l => l.id === strength)[0]; return level ? level.valid : false; };