diff --git a/ui/webui/src/apis/users.js b/ui/webui/src/apis/users.js
new file mode 100644
index 00000000000..a4d2bc6cc5a
--- /dev/null
+++ b/ui/webui/src/apis/users.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * 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 2.1 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, see .
+ */
+import cockpit from "cockpit";
+import { _setProperty } from "./helpers.js";
+
+const OBJECT_PATH = "/org/fedoraproject/Anaconda/Modules/Users";
+const INTERFACE_NAME = "org.fedoraproject.Anaconda.Modules.Users";
+
+const setProperty = (...args) => {
+ return _setProperty(UsersClient, OBJECT_PATH, INTERFACE_NAME, ...args);
+};
+
+export class UsersClient {
+ constructor (address) {
+ if (UsersClient.instance && (!address || UsersClient.instance.address === address)) {
+ return UsersClient.instance;
+ }
+
+ UsersClient.instance?.client.close();
+
+ UsersClient.instance = this;
+
+ this.client = cockpit.dbus(
+ INTERFACE_NAME,
+ { superuser: "try", bus: "none", address }
+ );
+ this.address = address;
+ }
+
+ init () {
+ this.client.addEventListener(
+ "close", () => console.error("Users client closed")
+ );
+ }
+}
+
+/**
+ * @param {Array.} users An array of user objects
+ */
+export const setUsers = (users) => {
+ return setProperty("Users", cockpit.variant("aa{sv}", users));
+};
diff --git a/ui/webui/src/components/AnacondaWizard.jsx b/ui/webui/src/components/AnacondaWizard.jsx
index eb6beff0d8a..9ae1ae3d446 100644
--- a/ui/webui/src/components/AnacondaWizard.jsx
+++ b/ui/webui/src/components/AnacondaWizard.jsx
@@ -39,6 +39,7 @@ import { getDefaultScenario } 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 { InstallationProgress } from "./installation/InstallationProgress.jsx";
import { ReviewConfiguration, ReviewConfigurationConfirmModal, getPageProps as getReviewConfigurationProps } from "./review/ReviewConfiguration.jsx";
import { exitGui } from "../helpers/exit.js";
@@ -50,6 +51,9 @@ import {
applyStorage,
resetPartitioning,
} from "../apis/storage_partitioning.js";
+import {
+ setUsers,
+} from "../apis/users.js";
import { SystemTypeContext, OsReleaseContext } from "./Common.jsx";
const _ = cockpit.gettext;
@@ -63,6 +67,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
const [stepNotification, setStepNotification] = useState();
const [storageEncryption, setStorageEncryption] = useState(getStorageEncryptionState());
const [storageScenarioId, setStorageScenarioId] = useState(window.sessionStorage.getItem("storage-scenario-id") || getDefaultScenario().id);
+ const [accounts, setAccounts] = useState(getAccountsState());
const [showWizard, setShowWizard] = useState(true);
const osRelease = useContext(OsReleaseContext);
const isBootIso = useContext(SystemTypeContext) === "BOOT_ISO";
@@ -143,6 +148,15 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
...getDiskEncryptionProps({ storageScenarioId })
}]
},
+ {
+ component: Accounts,
+ data: {
+ accounts,
+ setAccounts,
+ passwordPolicies: runtimeData.passwordPolicies,
+ },
+ ...getAccountsProps({ isBootIso })
+ },
{
component: ReviewConfiguration,
data: {
@@ -178,10 +192,14 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
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) => {
- const stepIdx = flattenedStepsIds.findIndex(s => s === stepId);
- const currentStepIdx = flattenedStepsIds.findIndex(s => s === currentStepId);
- return stepIdx <= currentStepIdx;
+ return stepId === currentStepId || isStepFollowedBy(stepId, currentStepId);
};
const createSteps = (stepsOrder) => {
@@ -224,8 +242,10 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
setIsFormValid(false);
}
- // Reset the applied partitioning when going back from review page
- if (prevStep.prevId === "installation-review") {
+ // 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)) &&
+ isStepFollowedBy(newStep.id, "accounts")) {
setIsFormDisabled(true);
resetPartitioning()
.then(
@@ -262,6 +282,7 @@ export const AnacondaWizard = ({ dispatch, storageData, localizationData, runtim
stepsOrder={stepsOrder}
storageEncryption={storageEncryption}
storageScenarioId={storageScenarioId}
+ accounts={accounts}
/>}
hideClose
mainAriaLabel={`${title} content`}
@@ -288,6 +309,7 @@ const Footer = ({
stepsOrder,
storageEncryption,
storageScenarioId,
+ accounts,
}) => {
const [nextWaitsConfirmation, setNextWaitsConfirmation] = useState(false);
const [quitWaitsConfirmation, setQuitWaitsConfirmation] = useState(false);
@@ -338,6 +360,9 @@ const Footer = ({
setStepNotification();
},
});
+ } else if (activeStep.id === "accounts") {
+ setUsers(accountsToDbusUsers(accounts));
+ onNext();
} else {
onNext();
}
diff --git a/ui/webui/src/components/Password.jsx b/ui/webui/src/components/Password.jsx
new file mode 100644
index 00000000000..e8690e5a8e6
--- /dev/null
+++ b/ui/webui/src/components/Password.jsx
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * 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 2.1 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, see .
+ */
+
+import cockpit from "cockpit";
+import React, { useState, useEffect, useMemo } from "react";
+import { debounce } from "throttle-debounce";
+
+import {
+ Button,
+ FormGroup,
+ FormHelperText,
+ HelperText,
+ HelperTextItem,
+ InputGroup,
+ InputGroupItem,
+ TextInput,
+} from "@patternfly/react-core";
+
+// eslint-disable-next-line camelcase
+import { password_quality } from "cockpit-components-password.jsx";
+import {
+ ExclamationCircleIcon,
+ ExclamationTriangleIcon,
+ CheckCircleIcon,
+ EyeIcon,
+ EyeSlashIcon
+} from "@patternfly/react-icons";
+
+const _ = cockpit.gettext;
+
+export const ruleLength = {
+ id: "length",
+ text: (policy) => cockpit.format(_("Must be at least $0 characters"), policy["min-length"].v),
+ check: (policy, password) => password.length >= policy["min-length"].v,
+ isError: true,
+};
+
+/* 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 = [{
+ id: "weak",
+ label: _("Weak"),
+ variant: "error",
+ icon: ,
+ lower_bound: 0,
+ higher_bound: minQualility - 1,
+ valid: !isStrict,
+ }];
+
+ if (minQualility <= 69) {
+ levels.push({
+ id: "medium",
+ label: _("Medium"),
+ variant: "warning",
+ icon: ,
+ lower_bound: minQualility,
+ higher_bound: 69,
+ valid: true,
+ });
+ }
+
+ levels.push({
+ id: "strong",
+ label: _("Strong"),
+ variant: "success",
+ icon: ,
+ lower_bound: Math.max(70, minQualility),
+ higher_bound: 100,
+ valid: true,
+ });
+
+ return levels;
+};
+
+const getRuleResults = (rules, policy, password) => {
+ return rules.map(rule => {
+ return {
+ id: rule.id,
+ text: rule.text(policy, password),
+ isSatisfied: password.length > 0 ? rule.check(policy, password) : null,
+ isError: rule.isError
+ };
+ });
+};
+
+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 = ({
+ idPrefix,
+ policy,
+ initialPassword,
+ passwordLabel,
+ onChange,
+ initialConfirmPassword,
+ confirmPasswordLabel,
+ onConfirmChange,
+ rules,
+ setIsValid,
+}) => {
+ const [passwordHidden, setPasswordHidden] = useState(true);
+ const [confirmHidden, setConfirmHidden] = useState(true);
+ const [_password, _setPassword] = useState(initialPassword);
+ const [_confirmPassword, _setConfirmPassword] = useState(initialConfirmPassword);
+ const [password, setPassword] = useState(initialPassword);
+ const [confirmPassword, setConfirmPassword] = useState(initialConfirmPassword);
+ const [passwordStrength, setPasswordStrength] = useState("");
+
+ useEffect(() => {
+ debounce(300, () => { setPassword(_password); onChange(_password) })();
+ }, [_password, onChange]);
+
+ useEffect(() => {
+ debounce(300, () => { setConfirmPassword(_confirmPassword); onConfirmChange(_confirmPassword) })();
+ }, [_confirmPassword, onConfirmChange]);
+
+ const ruleResults = useMemo(() => {
+ return getRuleResults(rules, policy, password);
+ }, [policy, password, rules]);
+
+ const ruleConfirmMatches = useMemo(() => {
+ return password.length > 0 ? password === confirmPassword : null;
+ }, [password, confirmPassword]);
+
+ 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(password, strengthLevels);
+ setPasswordStrength(_passwordStrength);
+ };
+ updatePasswordStrength();
+ }, [password, strengthLevels]);
+
+ useEffect(() => {
+ setIsValid(
+ rulesSatisfied(ruleResults) &&
+ ruleConfirmMatches &&
+ isValidStrength(passwordStrength, strengthLevels)
+ );
+ }, [setIsValid, ruleResults, ruleConfirmMatches, passwordStrength, strengthLevels]);
+
+ return (
+ <>
+
+
+
+ _setPassword(val)}
+ id={idPrefix + "-password-field"}
+ />
+
+
+ setPasswordHidden(!passwordHidden)}
+ aria-label={passwordHidden ? _("Show password") : _("Hide password")}
+ >
+ {passwordHidden ? : }
+
+
+
+
+
+ {ruleHelperItems}
+
+
+
+
+
+ _setConfirmPassword(val)}
+ id={idPrefix + "-password-confirm-field"}
+ />
+
+
+ setConfirmHidden(!confirmHidden)}
+ aria-label={confirmHidden ? _("Show confirmed password") : _("Hide confirmed password")}
+ >
+ {confirmHidden ? : }
+
+
+
+
+
+
+ {_("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;
+};
diff --git a/ui/webui/src/components/app.jsx b/ui/webui/src/components/app.jsx
index ea6939cdb90..564d4d46e7d 100644
--- a/ui/webui/src/components/app.jsx
+++ b/ui/webui/src/components/app.jsx
@@ -36,6 +36,7 @@ import { StorageClient, initDataStorage, startEventMonitorStorage } from "../api
import { PayloadsClient } from "../apis/payloads";
import { RuntimeClient, initDataRuntime, startEventMonitorRuntime } from "../apis/runtime";
import { NetworkClient, initDataNetwork, startEventMonitorNetwork } from "../apis/network.js";
+import { UsersClient } from "../apis/users";
import { setCriticalErrorAction } from "../actions/miscellaneous-actions.js";
@@ -78,6 +79,7 @@ export const Application = () => {
new RuntimeClient(address),
new BossClient(address),
new NetworkClient(address),
+ new UsersClient(address),
];
clients.forEach(c => c.init());
diff --git a/ui/webui/src/components/storage/DiskEncryption.jsx b/ui/webui/src/components/storage/DiskEncryption.jsx
index 04ad47f0f50..786427c714a 100644
--- a/ui/webui/src/components/storage/DiskEncryption.jsx
+++ b/ui/webui/src/components/storage/DiskEncryption.jsx
@@ -16,238 +16,38 @@
*/
import cockpit from "cockpit";
-import React, { useState, useEffect, useMemo } from "react";
-import { debounce } from "throttle-debounce";
+import React, { useState, useEffect } from "react";
import {
- Button,
Checkbox,
EmptyState,
+ EmptyStateHeader,
EmptyStateIcon,
+ EmptyStateFooter,
Form,
- FormGroup,
- FormHelperText,
- HelperText,
- HelperTextItem,
- InputGroup,
Spinner,
- TextInput,
TextContent,
TextVariants,
- Text, EmptyStateHeader, EmptyStateFooter, InputGroupItem,
+ Text,
} from "@patternfly/react-core";
-// eslint-disable-next-line camelcase
-import { password_quality } from "cockpit-components-password.jsx";
-import EyeIcon from "@patternfly/react-icons/dist/esm/icons/eye-icon";
-import EyeSlashIcon from "@patternfly/react-icons/dist/esm/icons/eye-slash-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 CheckCircleIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon";
-
import "./DiskEncryption.scss";
-const _ = cockpit.gettext;
-
-/* 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 = [{
- id: "weak",
- label: _("Weak"),
- variant: "error",
- icon: ,
- lower_bound: 0,
- higher_bound: minQualility - 1,
- valid: !isStrict,
- }];
-
- if (minQualility <= 69) {
- levels.push({
- id: "medium",
- label: _("Medium"),
- variant: "warning",
- icon: ,
- lower_bound: minQualility,
- higher_bound: 69,
- valid: true,
- });
- }
+import { PasswordFormFields, ruleLength } from "../Password.jsx";
- levels.push({
- id: "strong",
- label: _("Strong"),
- variant: "success",
- icon: ,
- lower_bound: Math.max(70, minQualility),
- higher_bound: 100,
- valid: true,
- });
+const _ = cockpit.gettext;
- return levels;
+const ruleAscii = {
+ id: "ascii",
+ text: (policy) => _("The passphrase you have provided contains non-ASCII characters. You may not be able to switch between keyboard layouts when typing it."),
+ check: (policy, password) => password.length > 0 && /^[\x20-\x7F]*$/.test(password),
+ isError: false,
};
export function getStorageEncryptionState (password = "", confirmPassword = "", encrypt = false) {
return { password, confirmPassword, encrypt };
}
-const passwordStrengthLabel = (idPrefix, strength, strengthLevels) => {
- const level = strengthLevels.filter(l => l.id === strength)[0];
- if (level) {
- return (
-
-
- {level.label}
-
-
- );
- }
-};
-
-// TODO create strengthLevels object with methods passed to the component ?
-const PasswordFormFields = ({
- idPrefix,
- policy,
- password,
- passwordLabel,
- onChange,
- passwordConfirm,
- passwordConfirmLabel,
- onConfirmChange,
- passwordStrength,
- ruleLength,
- ruleConfirmMatches,
- ruleAscii,
- strengthLevels,
-}) => {
- const [passwordHidden, setPasswordHidden] = useState(true);
- const [confirmHidden, setConfirmHidden] = useState(true);
- const [_password, _setPassword] = useState(password);
- const [_passwordConfirm, _setPasswordConfirm] = useState(passwordConfirm);
-
- useEffect(() => {
- debounce(300, () => onChange(_password))();
- }, [_password, onChange]);
-
- useEffect(() => {
- debounce(300, () => onConfirmChange(_passwordConfirm))();
- }, [_passwordConfirm, onConfirmChange]);
-
- return (
- <>
-
-
-
- _setPassword(val)}
- id={idPrefix + "-password-field"}
- />
-
-
- setPasswordHidden(!passwordHidden)}
- aria-label={passwordHidden ? _("Show password") : _("Hide password")}
- >
- {passwordHidden ? : }
-
-
-
-
-
-
- {cockpit.format(_("Must be at least $0 characters"), policy["min-length"].v)}
-
- {ruleAscii &&
-
- {_("The passphrase you have provided contains non-ASCII characters. You may not be able to switch between keyboard layouts when typing it.")}
- }
-
-
-
-
-
- _setPasswordConfirm(val)}
- id={idPrefix + "-password-confirm-field"}
- />
-
-
- setConfirmHidden(!confirmHidden)}
- aria-label={confirmHidden ? _("Show confirmed password") : _("Hide confirmed password")}
- >
- {confirmHidden ? : }
-
-
-
-
-
-
- {_("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;
-};
-
-const getRuleLength = (password, minLength) => {
- let ruleState = "indeterminate";
- if (password.length > 0 && password.length < minLength) {
- ruleState = "error";
- } else if (password.length >= minLength) {
- ruleState = "success";
- }
- return ruleState;
-};
-
-const getRuleConfirmMatches = (password, confirm) => (password.length > 0 ? (password === confirm ? "success" : "error") : "indeterminate");
-
const CheckDisksSpinner = (
{_("Checking storage configuration")}>} icon={ } headingLevel="h4" />
@@ -271,26 +71,9 @@ export const DiskEncryption = ({
}) => {
const [password, setPassword] = useState(storageEncryption.password);
const [confirmPassword, setConfirmPassword] = useState(storageEncryption.confirmPassword);
- const [passwordStrength, setPasswordStrength] = useState("");
const isEncrypted = storageEncryption.encrypt;
const luksPolicy = passwordPolicies.luks;
- const ruleConfirmMatches = useMemo(() => {
- return getRuleConfirmMatches(password, confirmPassword);
- }, [password, confirmPassword]);
-
- const ruleLength = useMemo(() => {
- return luksPolicy && getRuleLength(password, luksPolicy["min-length"].v);
- }, [password, luksPolicy]);
-
- const ruleAscii = useMemo(() => {
- return password.length > 0 && !/^[\x20-\x7F]*$/.test(password);
- }, [password]);
-
- const strengthLevels = useMemo(() => {
- return luksPolicy && getStrengthLevels(luksPolicy["min-quality"].v, luksPolicy["is-strict"].v);
- }, [luksPolicy]);
-
const encryptedDevicesCheckbox = content => (
);
useEffect(() => {
- if (!strengthLevels) {
- return;
- }
-
- const updatePasswordStrength = async () => {
- const _passwordStrength = await getPasswordStrength(password, strengthLevels);
- setPasswordStrength(_passwordStrength);
- };
- updatePasswordStrength();
- }, [password, strengthLevels]);
-
- useEffect(() => {
- const updateValidity = (isEncrypted) => {
- const passphraseValid = (
- ruleLength === "success" &&
- ruleConfirmMatches === "success" &&
- isValidStrength(passwordStrength, strengthLevels)
- );
- setIsFormValid(!isEncrypted || passphraseValid);
- };
-
- updateValidity(isEncrypted);
- }, [setIsFormValid, isEncrypted, ruleConfirmMatches, ruleLength, passwordStrength, strengthLevels]);
+ setIsFormValid(!isEncrypted);
+ }, [setIsFormValid, isEncrypted]);
useEffect(() => {
setStorageEncryption(se => ({ ...se, password }));
diff --git a/ui/webui/src/components/storage/DiskEncryption.scss b/ui/webui/src/components/storage/DiskEncryption.scss
index bb1272cb4af..1d908619860 100644
--- a/ui/webui/src/components/storage/DiskEncryption.scss
+++ b/ui/webui/src/components/storage/DiskEncryption.scss
@@ -1,4 +1,4 @@
-// Span disk encryption password fields to take slightly more width than the default
+// Limit the width of input fields
#disk-encryption-encrypt-devices ~ .pf-v5-c-check__body {
width: min(60ch, 100%);
}
diff --git a/ui/webui/src/components/users/Accounts.jsx b/ui/webui/src/components/users/Accounts.jsx
new file mode 100644
index 00000000000..8ab65260aea
--- /dev/null
+++ b/ui/webui/src/components/users/Accounts.jsx
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * 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 2.1 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, see .
+ */
+
+import cockpit from "cockpit";
+import React, { useState, useEffect } from "react";
+
+import {
+ Form,
+ FormGroup,
+ TextInput,
+ Title,
+} from "@patternfly/react-core";
+
+import "./Accounts.scss";
+
+import { PasswordFormFields, ruleLength } from "../Password.jsx";
+
+const _ = cockpit.gettext;
+
+export function getAccountsState (
+ fullName = "",
+ userAccount = "",
+ password = "",
+ confirmPassword = "",
+) {
+ return {
+ fullName,
+ userAccount,
+ password,
+ confirmPassword,
+ };
+}
+
+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),
+ groups: cockpit.variant("as", ["wheel"]),
+ }];
+};
+
+const CreateAccount = ({
+ idPrefix,
+ passwordPolicy,
+ setIsUserValid,
+ accounts,
+ setAccounts,
+}) => {
+ const [fullName, setFullName] = useState(accounts.fullName);
+ const [userAccount, setUserAccount] = useState(accounts.userAccount);
+ const [password, setPassword] = useState(accounts.password);
+ const [confirmPassword, setConfirmPassword] = useState(accounts.confirmPassword);
+ const [isPasswordValid, setIsPasswordValid] = useState(false);
+
+ useEffect(() => {
+ setIsUserValid(isPasswordValid && userAccount.length > 0);
+ }, [setIsUserValid, isPasswordValid, userAccount]);
+
+ const passphraseForm = (
+
+ );
+
+ useEffect(() => {
+ setAccounts(ac => ({ ...ac, fullName, userAccount, password, confirmPassword }));
+ }, [setAccounts, fullName, userAccount, password, confirmPassword]);
+
+ return (
+
+ );
+};
+
+export const Accounts = ({
+ idPrefix,
+ setIsFormValid,
+ passwordPolicies,
+ accounts,
+ setAccounts,
+}) => {
+ const [isUserValid, setIsUserValid] = useState();
+ useEffect(() => {
+ setIsFormValid(isUserValid);
+ }, [setIsFormValid, isUserValid]);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export const getPageProps = ({ isBootIso }) => {
+ return ({
+ id: "accounts",
+ label: _("Create Account"),
+ isHidden: !isBootIso,
+ title: null,
+ });
+};
diff --git a/ui/webui/src/components/users/Accounts.scss b/ui/webui/src/components/users/Accounts.scss
new file mode 100644
index 00000000000..dcc4da3b88f
--- /dev/null
+++ b/ui/webui/src/components/users/Accounts.scss
@@ -0,0 +1,10 @@
+// Limit the width of input fields
+#accounts-create-account {
+ width: min(60ch, 100%);
+}
+
+// FIXME: Undo when fixed upstream: https://github.com/patternfly/patternfly/issues/6032
+#accounts-create-account .pf-v5-c-form__group-label.pf-m-info {
+ flex-direction: column;
+ align-items: flex-start;
+}
diff --git a/ui/webui/test/check-basic b/ui/webui/test/check-basic
index 7056e69f27b..3b2f5021169 100755
--- a/ui/webui/test/check-basic
+++ b/ui/webui/test/check-basic
@@ -21,6 +21,7 @@ from installer import Installer
from language import Language
from review import Review
from storage import Storage
+from users import dbus_reset_users
from testlib import nondestructive, test_main, wait, Error # pylint: disable=import-error
from utils import pretend_live_iso, get_pretty_name
@@ -44,6 +45,7 @@ class TestBasic(anacondalib.VirtInstallMachineCase):
# Do not start the installation in non-destructive tests as this performs non revertible changes
# with the pages basically empty of common elements (as those are provided by the top-level installer widget)
# we at least iterate over them to check this works as expected
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# Ensure that the 'actual' UI process is running/
@@ -61,10 +63,12 @@ class TestBasic(anacondalib.VirtInstallMachineCase):
# Test that clicking on current step does not break navigation
i.click_step_on_sidebar()
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# Test going back
steps = [
+ i.steps.ACCOUNTS,
i.steps.DISK_CONFIGURATION,
i.steps.DISK_ENCRYPTION,
i.steps.DISK_CONFIGURATION,
@@ -93,7 +97,7 @@ class TestBasic(anacondalib.VirtInstallMachineCase):
b.wait_visible(f"#installation-back-btn:not([aria-disabled={True}]")
# For live media in the review screen language details should still be displayed
- i.reach(i.steps.REVIEW)
+ i.reach(i.steps.REVIEW, hidden_steps=[i.steps.ACCOUNTS])
r.check_language("English (United States)")
def testAboutModal(self):
diff --git a/ui/webui/test/check-review b/ui/webui/test/check-review
index e401e600bb2..7ec8392ea1d 100755
--- a/ui/webui/test/check-review
+++ b/ui/webui/test/check-review
@@ -22,6 +22,7 @@ from storage import Storage # pylint: disable=import-error
from review import Review
from language import Language
from progress import Progress
+from users import dbus_reset_users
from utils import add_public_key
from testlib import nondestructive, test_main # pylint: disable=import-error
@@ -40,6 +41,8 @@ class TestReview(anacondalib.VirtInstallMachineCase):
# After clicking 'Next' on the storage step, partitioning is done, thus changing the available space on the disk
# Since this is a non-destructive test we need to make sure the we reset partitioning to how it was before the test started
self.addCleanup(s.dbus_reset_partitioning)
+
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# check language is shown
@@ -71,6 +74,7 @@ class TestReview(anacondalib.VirtInstallMachineCase):
i.open()
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
i.begin_installation(should_fail=True, confirm_erase=False)
@@ -85,6 +89,7 @@ class TestReview(anacondalib.VirtInstallMachineCase):
# Set language to Macedonian that is now in the installer translated languages
# Go to review page after it
l.dbus_set_language("mk_MK.UTF-8")
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# test the macedonian language selected
diff --git a/ui/webui/test/check-storage b/ui/webui/test/check-storage
index 7f110632b78..9cda4588eed 100755
--- a/ui/webui/test/check-storage
+++ b/ui/webui/test/check-storage
@@ -19,7 +19,9 @@ import anacondalib
from installer import Installer
from storage import Storage
+from users import dbus_reset_users
from review import Review
+from password import Password
from testlib import nondestructive, test_main # pylint: disable=import-error
from storagelib import StorageHelpers # pylint: disable=import-error
from utils import pretend_live_iso
@@ -29,11 +31,9 @@ from utils import pretend_live_iso
class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
efi = False
- def set_valid_password(self, password="abcdefgh"):
- s = Storage(self.browser, self.machine)
-
- s.set_password(password)
- s.set_password_confirm(password)
+ def set_valid_password(self, password_ui, password="abcdefgh"):
+ password_ui.set_password(password)
+ password_ui.set_password_confirm(password)
def testLocalStandardDisks(self):
b = self.browser
@@ -133,6 +133,7 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
b = self.browser
i = Installer(b, self.machine)
s = Storage(b, self.machine)
+ p = Password(b, s.encryption_id_prefix)
i.open()
# Language selection
@@ -165,51 +166,51 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
)
# No password set
- s.check_pw_rule("min-chars", "indeterminate")
- s.check_pw_rule("match", "indeterminate")
+ p.check_pw_rule("length", "indeterminate")
+ p.check_pw_rule("match", "indeterminate")
i.check_next_disabled()
# Set pw which is too short
- s.set_password("abcd")
- s.check_pw_strength(None)
+ p.set_password("abcd")
+ p.check_pw_strength(None)
i.check_next_disabled()
- s.check_pw_rule("min-chars", "error")
- s.check_pw_rule("match", "error")
+ p.check_pw_rule("length", "error")
+ p.check_pw_rule("match", "error")
# Make the pw 8 chars long
- s.set_password("efgh", append=True, value_check=False)
+ p.set_password("efgh", append=True, value_check=False)
i.check_next_disabled()
- s.check_password("abcdefgh")
- s.check_pw_rule("min-chars", "success")
- s.check_pw_rule("match", "error")
- s.check_pw_strength("weak")
+ p.check_password("abcdefgh")
+ p.check_pw_rule("length", "success")
+ p.check_pw_rule("match", "error")
+ p.check_pw_strength("weak")
# Non-ASCII password
- s.set_password(8 * "š")
- s.check_password(8 * "š")
- s.check_pw_rule("min-chars", "success")
- s.check_pw_rule("match", "error")
- s.check_pw_rule("ascii", "warning")
- s.check_pw_strength("weak")
+ p.set_password(8 * "š")
+ p.check_password(8 * "š")
+ p.check_pw_rule("length", "success")
+ p.check_pw_rule("match", "error")
+ p.check_pw_rule("ascii", "warning")
+ p.check_pw_strength("weak")
# Valid ASCII password
- s.set_password("abcdefgh")
- s.check_password("abcdefgh")
+ p.set_password("abcdefgh")
+ p.check_password("abcdefgh")
# Set the password confirm
- s.set_password_confirm("abcdefg")
- s.check_pw_rule("match", "error")
- s.set_password_confirm("abcdefgh")
- s.check_pw_rule("match", "success")
- s.check_pw_rule("min-chars", "success")
- s.check_pw_strength("weak")
- s.check_password("abcdefgh")
- s.check_password_confirm("abcdefgh")
+ p.set_password_confirm("abcdefg")
+ p.check_pw_rule("match", "error")
+ p.set_password_confirm("abcdefgh")
+ p.check_pw_rule("match", "success")
+ p.check_pw_rule("length", "success")
+ p.check_pw_strength("weak")
+ p.check_password("abcdefgh")
+ p.check_password_confirm("abcdefgh")
i.check_next_disabled(disabled=False)
# Check setting strong password
- s.set_password("Rwce82ybF7dXtCzFumanchu!!!!!!!!")
- s.check_pw_strength("strong")
+ p.set_password("Rwce82ybF7dXtCzFumanchu!!!!!!!!")
+ p.check_pw_strength("strong")
# Test moving back after partitioning is applied,
# the partitioning should be reset.
@@ -217,6 +218,7 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
b = self.browser
i = Installer(b, self.machine)
s = Storage(b, self.machine)
+ p = Password(b, s.encryption_id_prefix)
i.open()
# Language selection
@@ -243,13 +245,13 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
s.check_encryption_selected(encrypt)
# Set valid password
- self.set_valid_password()
+ self.set_valid_password(p)
# Verify that the password is saved when moving forward and back
i.next()
i.back()
- s.check_password("abcdefgh")
- s.check_password_confirm("abcdefgh")
+ p.check_password("abcdefgh")
+ p.check_password_confirm("abcdefgh")
i.back()
# Storage Configuration
@@ -277,6 +279,7 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
# Go to Review step
i.open()
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# Read partitioning data after we went to Review step
@@ -295,10 +298,11 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
for _ in range(10):
s.dbus_create_partitioning("AUTOMATIC")
- # Go back to the previous page and re-enter the review screen.
+ # Go back to the Disk Configuration page and re-enter the review screen.
# This should create again a new partitioning object and apply it
# no matter how many partitioning objects were created before
i.back()
+ i.back()
i.reach(i.steps.REVIEW)
new_applied_partitioning = s.dbus_get_applied_partitioning()
new_created_partitioning = s.dbus_get_created_partitioning()
@@ -306,7 +310,7 @@ class TestStorage(anacondalib.VirtInstallMachineCase, StorageHelpers):
self.assertEqual(len(created_partitioning) + 11, len(new_created_partitioning))
self.assertEqual(new_applied_partitioning, new_created_partitioning[-1])
- # The applied partitioning should be reset when going back at any step from review page
+ # The applied partitioning should be reset also when going back to installation method
i.click_step_on_sidebar(i.steps.INSTALLATION_METHOD)
new_applied_partitioning = s.dbus_get_applied_partitioning()
self.assertEqual(new_applied_partitioning, "")
@@ -410,8 +414,10 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
"mount-point-mapping-table",
)
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
+
# verify review screen
r.check_disk(dev, "16.1 GB vda (0x1af4)")
@@ -422,6 +428,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
applied_partitioning = s.dbus_get_applied_partitioning()
# When adding a new partition a new partitioning should be created
+ i.back()
i.back(previous_page=i.steps.CUSTOM_MOUNT_POINT)
i.back()
@@ -534,6 +541,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
s.select_mountpoint_row_mountpoint(3, "/home")
s.check_mountpoint_row(3, "/home", f"{dev2}1", False, "xfs")
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# verify review screen
@@ -620,6 +628,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
b.wait_in_text(f"{selector} .pf-v5-c-select__toggle-text", "luks")
s.check_mountpoint_row_format_type(2, "xfs")
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
r.check_in_disk_row(dev1, 2, "luks-")
@@ -666,6 +675,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
s.select_mountpoint_row_device(2, "encryptedraid")
s.check_mountpoint_row_format_type(2, "xfs")
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
r.check_disk(dev, "16.1 GB vda (0x1af4)")
@@ -718,6 +728,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
b.wait_in_text(f"{selector} .pf-v5-c-select__toggle-text", "luks")
s.check_mountpoint_row_format_type(2, "xfs")
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
r.check_disk(dev, "16.1 GB vda (0x1af4)")
@@ -778,6 +789,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
s.select_mountpoint_row_reformat(1)
s.check_mountpoint_row_reformat(1, True)
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# verify review screen
@@ -788,6 +800,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
r.check_disk_row(dev, 3, "home, 15.0 GB: mount, /home")
r.check_disk_row_not_present(dev, f"unused")
+ i.back()
i.back(previous_page=i.steps.CUSTOM_MOUNT_POINT)
i.back()
@@ -939,6 +952,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
s.select_mountpoint_row_reformat(1)
s.check_mountpoint_row_reformat(1, True)
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# verify review screen
@@ -950,6 +964,7 @@ class TestStorageMountPoints(anacondalib.VirtInstallMachineCase, StorageHelpers)
r.check_disk_row(disk, 3, f"{vgname}-home, 8.12 GB: mount, /home")
r.check_disk_row(disk, 4, f"{vgname}-swap, 902 MB: mount, swap")
+ i.back()
i.back(previous_page=i.steps.CUSTOM_MOUNT_POINT)
# remove the /home row and check that row 3 is now swap
@@ -1027,6 +1042,7 @@ class TestStorageMountPointsEFI(anacondalib.VirtInstallMachineCase):
s.select_mountpoint_row_device(3, f"{dev}3")
s.check_mountpoint_row_format_type(3, "xfs")
+ self.addCleanup(lambda: dbus_reset_users(self.machine))
i.reach(i.steps.REVIEW)
# verify review screen
diff --git a/ui/webui/test/check-users b/ui/webui/test/check-users
new file mode 100755
index 00000000000..5d6d3601db0
--- /dev/null
+++ b/ui/webui/test/check-users
@@ -0,0 +1,51 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# 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 2.1 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, see .
+
+import anacondalib
+
+from installer import Installer
+from users import Users, CREATE_ACCOUNT_ID_PREFIX, create_user
+from testlib import nondestructive, test_main # pylint: disable=import-error
+
+@nondestructive
+class TestUsers(anacondalib.VirtInstallMachineCase):
+ def setUp(self):
+ super().setUp()
+
+ def testBasic(self):
+ b = self.browser
+ i = Installer(b, self.machine)
+ u = Users(b, self.machine)
+
+ i.open()
+
+ i.reach(i.steps.ACCOUNTS)
+ create_user(b, self.machine)
+ b.assert_pixels(
+ "#app",
+ "users-step-basic",
+ ignore=["#betanag-icon"],
+ )
+ i.reach(i.steps.REVIEW)
+
+ 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)
+
+if __name__ == '__main__':
+ test_main()
diff --git a/ui/webui/test/end2end/storage_encryption.py b/ui/webui/test/end2end/storage_encryption.py
index 703c163e765..caa69c62605 100755
--- a/ui/webui/test/end2end/storage_encryption.py
+++ b/ui/webui/test/end2end/storage_encryption.py
@@ -38,11 +38,11 @@ def configure_storage_encryption(self):
self._storage.set_encryption_selected(True)
self._storage.check_encryption_selected(True)
- self._storage.set_password(self.luks_pass)
- self._storage.check_password(self.luks_pass)
- self._storage.set_password_confirm(self.luks_pass)
- self._storage.check_password_confirm(self.luks_pass)
- self._storage.check_pw_strength('weak')
+ self._storage_password.set_password(self.luks_pass)
+ self._storage_password.check_password(self.luks_pass)
+ self._storage_password.set_password_confirm(self.luks_pass)
+ self._storage_password.check_password_confirm(self.luks_pass)
+ self._storage_password.check_pw_strength('weak')
@log_step()
def post_install_step(self):
diff --git a/ui/webui/test/end2end/wizard_navigation.py b/ui/webui/test/end2end/wizard_navigation.py
index 0d24fd4d944..d1bbf239fcd 100755
--- a/ui/webui/test/end2end/wizard_navigation.py
+++ b/ui/webui/test/end2end/wizard_navigation.py
@@ -45,6 +45,8 @@ def test_wizard_navigation(self):
self._installer.next()
self.configure_storage_encryption()
self._installer.next()
+ self.check_users_screen()
+ self._installer.next()
# Finish installation
self.check_review_screen()
self._installer.begin_installation()
diff --git a/ui/webui/test/helpers/end2end.py b/ui/webui/test/helpers/end2end.py
index 02a2ccef42c..65008fbf8c5 100644
--- a/ui/webui/test/helpers/end2end.py
+++ b/ui/webui/test/helpers/end2end.py
@@ -30,6 +30,8 @@
from storage import Storage
from review import Review
from progress import Progress
+from password import Password
+from users import create_user
from utils import add_public_key
from testlib import MachineCase # pylint: disable=import-error
from machine_install import VirtInstallMachine
@@ -44,6 +46,7 @@ def setUp(self):
self._installer = Installer(self.browser, self.machine)
self._language = Language(self.browser, self.machine)
self._storage = Storage(self.browser, self.machine)
+ self._storage_password = Password(self.browser, self._storage.encryption_id_prefix)
self._review = Review(self.browser)
self._progress = Progress(self.browser)
self.__installation_finished = False
@@ -75,6 +78,9 @@ def configure_storage_encryption(self):
def check_review_screen(self):
pass
+ def check_users_screen(self):
+ create_user(self.browser, self.machine)
+
def monitor_progress(self):
self._progress.wait_done()
@@ -101,6 +107,8 @@ def run_integration_test(self):
self._installer.next()
self.configure_storage_encryption()
self._installer.next()
+ self.check_users_screen()
+ self._installer.next()
self.check_review_screen()
self._installer.begin_installation()
self.monitor_progress()
diff --git a/ui/webui/test/helpers/installer.py b/ui/webui/test/helpers/installer.py
index 9867f21c230..7c0e57cc1db 100644
--- a/ui/webui/test/helpers/installer.py
+++ b/ui/webui/test/helpers/installer.py
@@ -18,6 +18,8 @@
from time import sleep
from step_logger import log_step
+from users import create_user
+
class InstallerSteps(UserDict):
WELCOME = "installation-language"
@@ -25,17 +27,22 @@ class InstallerSteps(UserDict):
CUSTOM_MOUNT_POINT = "mount-point-mapping"
DISK_CONFIGURATION = "disk-configuration"
DISK_ENCRYPTION = "disk-encryption"
+ ACCOUNTS = "accounts"
REVIEW = "installation-review"
PROGRESS = "installation-progress"
_steps_jump = {}
_steps_jump[WELCOME] = INSTALLATION_METHOD
_steps_jump[INSTALLATION_METHOD] = [DISK_ENCRYPTION, CUSTOM_MOUNT_POINT]
- _steps_jump[DISK_ENCRYPTION] = REVIEW
- _steps_jump[CUSTOM_MOUNT_POINT] = REVIEW
+ _steps_jump[DISK_ENCRYPTION] = ACCOUNTS
+ _steps_jump[CUSTOM_MOUNT_POINT] = ACCOUNTS
+ _steps_jump[ACCOUNTS] = REVIEW
_steps_jump[REVIEW] = PROGRESS
_steps_jump[PROGRESS] = []
+ _steps_callbacks = {}
+ _steps_callbacks[ACCOUNTS] = create_user
+
class Installer():
def __init__(self, browser, machine):
self.browser = browser
@@ -58,7 +65,8 @@ def begin_installation(self, should_fail=False, confirm_erase=True):
else:
self.wait_current_page(self.steps._steps_jump[current_page])
- def reach(self, target_page):
+ def reach(self, target_page, hidden_steps=None):
+ hidden_steps = hidden_steps or []
path = []
prev_pages = [target_page]
current_page = self.get_current_page()
@@ -70,7 +78,10 @@ def reach(self, target_page):
while self.get_current_page() != target_page:
next_page = path.pop()
- self.next(next_page=next_page)
+ if next_page not in hidden_steps:
+ self.next(next_page=next_page)
+ if next_page in self.steps._steps_callbacks:
+ self.steps._steps_callbacks[next_page](self.browser, self.machine)
@log_step()
def next(self, should_fail=False, next_page=""):
diff --git a/ui/webui/test/helpers/password.py b/ui/webui/test/helpers/password.py
new file mode 100644
index 00000000000..55324bfc67e
--- /dev/null
+++ b/ui/webui/test/helpers/password.py
@@ -0,0 +1,74 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# 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 2.1 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, see .
+
+import os
+import sys
+
+HELPERS_DIR = os.path.dirname(__file__)
+sys.path.append(HELPERS_DIR)
+
+from step_logger import log_step
+
+class Password():
+ def __init__(self, browser, id_prefix):
+ self.browser = browser
+ self.id_prefix = id_prefix
+
+ @log_step(snapshot_before=True)
+ def check_pw_rule(self, rule, value):
+ sel = f"#{self.id_prefix}-password-rule-" + rule
+ cls_value = "pf-m-" + value
+ self.browser.wait_visible(sel)
+ self.browser.wait_attr_contains(sel, "class", cls_value)
+
+ @log_step(snapshot_before=True)
+ def set_password(self, password, append=False, value_check=True):
+ sel = f"#{self.id_prefix}-password-field"
+ self.browser.set_input_text(sel, password, append=append, value_check=value_check)
+
+ @log_step(snapshot_before=True)
+ def check_password(self, password):
+ sel = f"#{self.id_prefix}-password-field"
+ self.browser.wait_val(sel, password)
+
+ @log_step(snapshot_before=True)
+ def set_password_confirm(self, password):
+ sel = f"#{self.id_prefix}-password-confirm-field"
+ self.browser.set_input_text(sel, password)
+
+ @log_step(snapshot_before=True)
+ def check_password_confirm(self, password):
+ sel = f"#{self.id_prefix}-password-confirm-field"
+ self.browser.wait_val(sel, password)
+
+ @log_step(snapshot_before=True)
+ def check_pw_strength(self, strength):
+ sel = f"#{self.id_prefix}-password-strength-label"
+
+ if strength is None:
+ self.browser.wait_not_present(sel)
+ return
+
+ variant = ""
+ if strength == "weak":
+ variant = "error"
+ elif strength == "medium":
+ variant = "warning"
+ elif strength == "strong":
+ variant = "success"
+
+ self.browser.wait_attr_contains(sel, "class", "pf-m-" + variant)
diff --git a/ui/webui/test/helpers/storage.py b/ui/webui/test/helpers/storage.py
index 2274b7fa990..604998fe8cd 100644
--- a/ui/webui/test/helpers/storage.py
+++ b/ui/webui/test/helpers/storage.py
@@ -117,13 +117,15 @@ def check_disk_visible(self, disk, visible=True):
class StorageEncryption():
+ encryption_id_prefix = "disk-encryption"
+
def __init__(self, browser, machine):
self.browser = browser
self.machine = machine
@log_step(snapshot_before=True)
def check_encryption_selected(self, selected):
- sel = "#disk-encryption-encrypt-devices"
+ sel = f"#{self.encryption_id_prefix}-encrypt-devices"
if selected:
self.browser.wait_visible(sel + ':checked')
else:
@@ -131,54 +133,9 @@ def check_encryption_selected(self, selected):
@log_step(snapshot_before=True)
def set_encryption_selected(self, selected):
- sel = "#disk-encryption-encrypt-devices"
+ sel = f"#{self.encryption_id_prefix}-encrypt-devices"
self.browser.set_checked(sel, selected)
- @log_step(snapshot_before=True)
- def check_pw_rule(self, rule, value):
- sel = "#disk-encryption-password-rule-" + rule
- cls_value = "pf-m-" + value
- self.browser.wait_visible(sel)
- self.browser.wait_attr_contains(sel, "class", cls_value)
-
- @log_step(snapshot_before=True)
- def set_password(self, password, append=False, value_check=True):
- sel = "#disk-encryption-password-field"
- self.browser.set_input_text(sel, password, append=append, value_check=value_check)
-
- @log_step(snapshot_before=True)
- def check_password(self, password):
- sel = "#disk-encryption-password-field"
- self.browser.wait_val(sel, password)
-
- @log_step(snapshot_before=True)
- def set_password_confirm(self, password):
- sel = "#disk-encryption-password-confirm-field"
- self.browser.set_input_text(sel, password)
-
- @log_step(snapshot_before=True)
- def check_password_confirm(self, password):
- sel = "#disk-encryption-password-confirm-field"
- self.browser.wait_val(sel, password)
-
- @log_step(snapshot_before=True)
- def check_pw_strength(self, strength):
- sel = "#disk-encryption-password-strength-label"
-
- if strength is None:
- self.browser.wait_not_present(sel)
- return
-
- variant = ""
- if strength == "weak":
- variant = "error"
- elif strength == "medium":
- variant = "warning"
- elif strength == "strong":
- variant = "success"
-
- self.browser.wait_attr_contains(sel, "class", "pf-m-" + variant)
-
class StorageUtils(StorageDestination):
def __init__(self, browser, machine):
diff --git a/ui/webui/test/helpers/users.py b/ui/webui/test/helpers/users.py
new file mode 100644
index 00000000000..e6a53f2dd32
--- /dev/null
+++ b/ui/webui/test/helpers/users.py
@@ -0,0 +1,80 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# 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 2.1 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, see .
+
+import os
+import sys
+
+HELPERS_DIR = os.path.dirname(__file__)
+sys.path.append(HELPERS_DIR)
+
+from password import Password
+from step_logger import log_step
+
+
+USERS_SERVICE = "org.fedoraproject.Anaconda.Modules.Users"
+USERS_INTERFACE = USERS_SERVICE
+USERS_OBJECT_PATH = "/org/fedoraproject/Anaconda/Modules/Users"
+
+CREATE_ACCOUNT_ID_PREFIX = "accounts-create-account"
+
+
+class UsersDBus():
+ def __init__(self, machine):
+ self.machine = machine
+ self._bus_address = self.machine.execute("cat /run/anaconda/bus.address")
+
+ def dbus_get_users(self):
+ ret = self.machine.execute(f'busctl --address="{self._bus_address}" \
+ get-property \
+ {USERS_SERVICE} \
+ {USERS_OBJECT_PATH} \
+ {USERS_INTERFACE} Users')
+
+ return ret
+
+ def dbus_clear_users(self):
+ self.machine.execute(f'busctl --address="{self._bus_address}" \
+ set-property \
+ {USERS_SERVICE} \
+ {USERS_OBJECT_PATH} \
+ {USERS_INTERFACE} Users aa{{sv}} 0')
+
+
+class Users(UsersDBus):
+ def __init__(self, browser, machine):
+ self.browser = browser
+
+ UsersDBus.__init__(self, machine)
+
+ @log_step(snapshot_before=True)
+ def set_user_account(self, user_account, append=False, value_check=True):
+ sel = "#accounts-create-account-user-account"
+ self.browser.set_input_text(sel, user_account, append=append, value_check=value_check)
+
+
+def create_user(browser, machine):
+ p = Password(browser, CREATE_ACCOUNT_ID_PREFIX)
+ u = Users(browser, machine)
+
+ password = "password"
+ p.set_password(password)
+ p.set_password_confirm(password)
+ u.set_user_account("tester")
+
+
+def dbus_reset_users(machine):
+ UsersDBus(machine).dbus_clear_users()
diff --git a/ui/webui/test/reference b/ui/webui/test/reference
index e05382c968a..926f5923b79 160000
--- a/ui/webui/test/reference
+++ b/ui/webui/test/reference
@@ -1 +1 @@
-Subproject commit e05382c968a80a76c717eca43882e12f982bff22
+Subproject commit 926f5923b7994a2858fb0b104936989ad60b5bd4