diff --git a/src/components/AnacondaWizard.jsx b/src/components/AnacondaWizard.jsx index cbbe264d89..31a09421b6 100644 --- a/src/components/AnacondaWizard.jsx +++ b/src/components/AnacondaWizard.jsx @@ -25,25 +25,22 @@ import { PageSection, PageSectionTypes, PageSectionVariants, - Stack -} from "@patternfly/react-core"; -import { + Stack, + useWizardContext, Wizard, - WizardFooter, - WizardContextConsumer -} from "@patternfly/react-core/deprecated"; + WizardFooterWrapper, + WizardStep +} from "@patternfly/react-core"; -import { AnacondaPage } from "./AnacondaPage.jsx"; import { InstallationMethod, getPageProps as getInstallationMethodProps } from "./storage/InstallationMethod.jsx"; -import { getDefaultScenario } from "./storage/InstallationScenario.jsx"; +import { getDefaultScenario, getScenario } from "./storage/InstallationScenario.jsx"; import { MountPointMapping, getPageProps as getMountPointMappingProps } from "./storage/MountPointMapping.jsx"; import { DiskEncryption, getStorageEncryptionState, getPageProps as getDiskEncryptionProps } from "./storage/DiskEncryption.jsx"; import { InstallationLanguage, getPageProps as getInstallationLanguageProps } from "./localization/InstallationLanguage.jsx"; -import { Accounts, getPageProps as getAccountsProps, getAccountsState, accountsToDbusUsers } from "./users/Accounts.jsx"; +import { Accounts, getPageProps as getAccountsProps, getAccountsState, accountsToDbusUsers, cryptUserPassword } from "./users/Accounts.jsx"; import { InstallationProgress } from "./installation/InstallationProgress.jsx"; import { ReviewConfiguration, ReviewConfigurationConfirmModal, getPageProps as getReviewConfigurationProps } from "./review/ReviewConfiguration.jsx"; import { exitGui } from "../helpers/exit.js"; -import { usePageLocation } from "hooks"; import { getRequiredMountPoints, } from "../apis/storage_devicetree.js"; @@ -55,6 +52,7 @@ import { setUsers, } from "../apis/users.js"; import { SystemTypeContext, OsReleaseContext } from "./Common.jsx"; +import { AnacondaPage } from "./AnacondaPage.jsx"; const _ = cockpit.gettext; const N_ = cockpit.noop; @@ -172,6 +170,13 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim }, ]; + const componentProps = { + isFormDisabled, + onCritFail, + setIsFormDisabled, + setIsFormValid, + }; + const getFlattenedStepsIds = (steps) => { const stepIds = []; for (const step of steps) { @@ -189,63 +194,58 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim }; const flattenedStepsIds = getFlattenedStepsIds(stepsOrder); - const { path } = usePageLocation(); - const firstStepId = stepsOrder.filter(step => !step.isHidden)[0].id; - const currentStepId = path[0] || firstStepId; - const isStepFollowedBy = (earlierStepId, laterStepId) => { const earlierStepIdx = flattenedStepsIds.findIndex(s => s === earlierStepId); const laterStepIdx = flattenedStepsIds.findIndex(s => s === laterStepId); return earlierStepIdx < laterStepIdx; }; - const canJumpToStep = (stepId, currentStepId) => { - return stepId === currentStepId || isStepFollowedBy(stepId, currentStepId); - }; - - const createSteps = (stepsOrder) => { - const steps = stepsOrder.filter(s => !s.isHidden).map(s => { - let step = ({ + const createSteps = (stepsOrder, componentProps) => { + return stepsOrder.filter(s => !s.isHidden).map(s => { + let stepProps = { id: s.id, + isHidden: s.isHidden, name: s.label, stepNavItemProps: { id: s.id }, - canJumpTo: canJumpToStep(s.id, currentStepId), - }); + ...(s.steps?.length && { isExpandable: true }), + }; if (s.component) { - step = ({ - ...step, - component: ( + stepProps = { + children: ( setStepNotification({ step: s.id, ...ex })} - isFormDisabled={isFormDisabled} - setIsFormDisabled={setIsFormDisabled} + {...componentProps} {...s.data} /> ), - }); + ...stepProps + }; } else if (s.steps) { - step.steps = createSteps(s.steps); + const subSteps = createSteps(s.steps, componentProps); + stepProps = { + ...stepProps, + steps: [...subSteps] + }; } - return step; + return ( + + ); }); - return steps; }; - const steps = createSteps(stepsOrder); + const steps = createSteps(stepsOrder, componentProps); const goToStep = (newStep, prevStep) => { - if (prevStep.prevId !== newStep.id) { + if (prevStep.id !== newStep.id) { // first reset validation state to default setIsFormValid(false); } // Reset the applied partitioning when going back from a step after creating partitioning to a step // before creating partitioning. - if ((prevStep.prevId === "accounts" || isStepFollowedBy("accounts", prevStep.prevId)) && + if ((prevStep.id === "accounts" || isStepFollowedBy("accounts", prevStep.id)) && isStepFollowedBy(newStep.id, "accounts")) { setIsFormDisabled(true); resetPartitioning() @@ -271,6 +271,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim } - hideClose - mainAriaLabel={`${title} content`} - navAriaLabel={`${title} steps`} - onBack={goToStep} - onGoToStep={goToStep} - onNext={goToStep} - steps={steps} - isNavExpandable - /> + onStepChange={((event, currentStep, prevStep) => goToStep(currentStep, prevStep))} + > + {steps} + ); }; @@ -314,9 +310,10 @@ const Footer = ({ }) => { const [nextWaitsConfirmation, setNextWaitsConfirmation] = useState(false); const [quitWaitsConfirmation, setQuitWaitsConfirmation] = useState(false); + const { activeStep, goToNextStep, goToPrevStep } = useWizardContext(); const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO"; - const goToNextStep = (activeStep, onNext) => { + const onNext = (activeStep, goToNextStep) => { // first reset validation state to default setIsFormValid(true); @@ -330,7 +327,7 @@ const Footer = ({ setStepNotification({ step: activeStep.id, ...ex }); }, onSuccess: () => { - onNext(); + goToNextStep(); // Reset the state after the onNext call. Otherwise, // React will try to render the current step again. @@ -353,7 +350,7 @@ const Footer = ({ setStepNotification({ step: activeStep.id, ...ex }); }, onSuccess: () => { - onNext(); + goToNextStep(); // Reset the state after the onNext call. Otherwise, // React will try to render the current step again. @@ -362,85 +359,88 @@ const Footer = ({ }, }); } else if (activeStep.id === "accounts") { - setUsers(accountsToDbusUsers(accounts)); - onNext(); + cryptUserPassword(accounts.password) + .then(cryptedPassword => { + const users = accountsToDbusUsers({ ...accounts, password: cryptedPassword }); + setUsers(users); + onNext(); + }, onCritFail({ context: N_("Password ecryption failed.") })); } else { - onNext(); + goToNextStep(); } }; - const goToPreviousStep = (activeStep, onBack, errorHandler) => { + const onBack = (goToPrevStep, errorHandler) => { // first reset validation state to default - setIsFormValid(true); - onBack(); + setIsFormValid(false); + goToPrevStep(); }; + const isFirstScreen = ( + activeStep.id === "installation-language" || (activeStep.id === "installation-method" && !isBootIso) + ); + + const nextButtonText = ( + activeStep.id === "installation-review" + ? getScenario(storageScenarioId).buttonLabel + : _("Next") + ); + + const footerHelperText = stepsOrder.find(step => step.id === activeStep.id)?.footerHelperText; + return ( - - - {({ activeStep, onNext, onBack }) => { - const currentStep = stepsOrder.find(s => s.id === activeStep.id); - const footerHelperText = currentStep?.footerHelperText; - const isFirstScreen = stepsOrder.filter(step => !step.isHidden)[0].id === activeStep.id; - const nextButtonText = currentStep?.nextButtonText || _("Next"); - const nextButtonVariant = currentStep?.nextButtonVariant || "primary"; - - return ( - - {activeStep.id === "installation-review" && - nextWaitsConfirmation && - { setShowWizard(false); cockpit.location.go(["installation-progress"]) }} - setNextWaitsConfirmation={setNextWaitsConfirmation} - storageScenarioId={storageScenarioId} - />} - {quitWaitsConfirmation && - } - {footerHelperText} - - - - - - - ); - }} - - + + + {activeStep.id === "installation-review" && + nextWaitsConfirmation && + { setShowWizard(false); cockpit.location.go(["installation-progress"]) }} + setNextWaitsConfirmation={setNextWaitsConfirmation} + storageScenarioId={storageScenarioId} + />} + {quitWaitsConfirmation && + } + {footerHelperText} + + + + + + + ); }; diff --git a/src/components/review/ReviewConfiguration.jsx b/src/components/review/ReviewConfiguration.jsx index a597759717..3080efda0c 100644 --- a/src/components/review/ReviewConfiguration.jsx +++ b/src/components/review/ReviewConfiguration.jsx @@ -187,7 +187,7 @@ export const ReviewConfiguration = ({ deviceData, diskSelection, language, local ); }; -export const ReviewConfigurationConfirmModal = ({ idPrefix, onNext, setNextWaitsConfirmation, storageScenarioId }) => { +export const ReviewConfigurationConfirmModal = ({ idPrefix, goToNextStep, setNextWaitsConfirmation, storageScenarioId }) => { const scenario = getScenario(storageScenarioId); return ( { setNextWaitsConfirmation(false); - onNext(); + goToNextStep(); }} variant={scenario.buttonVariant} > diff --git a/src/components/users/Accounts.jsx b/src/components/users/Accounts.jsx index 8ab65260ae..bce838273b 100644 --- a/src/components/users/Accounts.jsx +++ b/src/components/users/Accounts.jsx @@ -17,6 +17,8 @@ import cockpit from "cockpit"; import React, { useState, useEffect } from "react"; +import * as python from "python.js"; +import encryptUserPw from "../../scripts/encrypt-user-pw.py"; import { Form, @@ -45,12 +47,17 @@ export function getAccountsState ( }; } +export const cryptUserPassword = async (password) => { + const crypted = await python.spawn(encryptUserPw, password, { err: "message", environ: ["LC_ALL=C.UTF-8"] }); + return crypted; +}; + export const accountsToDbusUsers = (accounts) => { return [{ name: cockpit.variant("s", accounts.userAccount || ""), gecos: cockpit.variant("s", accounts.fullName || ""), password: cockpit.variant("s", accounts.password || ""), - "is-crypted": cockpit.variant("b", false), + "is-crypted": cockpit.variant("b", true), groups: cockpit.variant("as", ["wheel"]), }]; }; diff --git a/src/scripts/encrypt-user-pw.py b/src/scripts/encrypt-user-pw.py new file mode 100644 index 0000000000..91ee9af413 --- /dev/null +++ b/src/scripts/encrypt-user-pw.py @@ -0,0 +1,42 @@ +import crypt +from random import SystemRandom as sr +import sys + +# Using the function from pyanaconda/core/users.py + +def crypt_password(password): + """Crypt a password. + + Process a password with appropriate salted one-way algorithm. + + :param str password: password to be crypted + :returns: crypted representation of the original password + :rtype: str + """ + # yescrypt is not supported by Python's crypt module, + # so we need to generate the setting ourselves + b64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + setting = "$y$j9T$" + "".join(sr().choice(b64) for _sc in range(24)) + + # and try to compute the password hash using our yescrypt setting + try: + cryptpw = crypt.crypt(password, setting) + + # Fallback to sha512crypt, if yescrypt is not supported + except OSError: + sys.stderr.write("yescrypt is not supported, falling back to sha512crypt\n") + try: + cryptpw = crypt.crypt(password, crypt.METHOD_SHA512) + except OSError as exc: + raise RuntimeError( + "Unable to encrypt password: unsupported algorithm {}".format(crypt.METHOD_SHA512) + ) from exc + + return cryptpw + + +try: + print(crypt_password(sys.argv[1]), end="") +except Exception as e: + sys.stderr.write(str(e) + "\n") + sys.exit(1) diff --git a/test/check-users b/test/check-users index 2d44d7f978..59fe08e848 100755 --- a/test/check-users +++ b/test/check-users @@ -44,8 +44,7 @@ class TestUsers(anacondalib.VirtInstallMachineCase): users = u.dbus_get_users() self.assertIn('"groups" as 1 "wheel"', users) - self.assertIn('"is-crypted" b false', users) - self.assertIn('"password" s "password"', users) + self.assertIn('"is-crypted" b true', users) if __name__ == '__main__': test_main()