Skip to content

Commit

Permalink
refactor(web): simplify page to enforce root password (#1821)
Browse files Browse the repository at this point in the history
## Problem

During a review meeting on December 9th, it was argue that the [new
screen for enforcing at least one root authentication
method](#1787) could be
intimidating for some users, mainly due to the inclusion of the [SSH
public key](https://www.ssh.com/academy/ssh/public-key-authentication)
input field.

## Solution

In the same meeting, it was suggested to hide the SSH public key input
and display it only upon user request, taking the opportunity for also
providing a brief explanation of what an SSH public key is.

While this was a good idea, after testing a couple of approaches it
became apparent that there were several corner cases, all of them
leading to add too much user interaction to an already intrusive screen
that should be kept as minimalist as possible.

Thus, the input has been completely removed instead. Now, the page ask
for the root password and provides information to let users aware about
the possibility to change the root authentication method at any time
before installation from the _Users_ section.

---

Related to https://trello.com/c/GQytAUEU (internal link)
  • Loading branch information
dgdavid authored Dec 9, 2024
2 parents 6501805 + 96cb8ac commit e24a29e
Show file tree
Hide file tree
Showing 3 changed files with 28 additions and 127 deletions.
6 changes: 0 additions & 6 deletions web/src/assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,3 @@ button.remove-link:hover {
--pf-v5-c-notification-drawer__list-item--before--BackgroundColor: none;
}
}

form#rootAuthMethods {
.pf-v5-c-file-upload__file-select {
display: none;
}
}
38 changes: 5 additions & 33 deletions web/src/components/users/RootAuthMethodsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,12 @@ jest.mock("~/queries/users", () => ({
}));

describe("RootAuthMethodsPage", () => {
it("allows setting a root authentication method", async () => {
it("allows setting a root password", async () => {
const { user } = installerRender(<RootAuthMethodsPage />);
const passwordInput = screen.getByLabelText("Password");
const sshKeyTextarea = screen.getByLabelText("SSH public key");
const passwordInput = screen.getByLabelText("Password for root user");
const acceptButton = screen.getByRole("button", { name: "Accept" });

// There must be an upload button too (behavior not covered here);
screen.getByRole("button", { name: "upload" });

// The Accept button must be enable only when at least one authentication
// method is defined
// The Accept button must be enable only when password has some value
expect(acceptButton).toHaveAttribute("disabled");

await user.type(passwordInput, "s3cr3t");
Expand All @@ -52,32 +47,9 @@ describe("RootAuthMethodsPage", () => {
await user.clear(passwordInput);
expect(acceptButton).toHaveAttribute("disabled");

await user.type(sshKeyTextarea, "FAKE SSH KEY");
expect(acceptButton).not.toHaveAttribute("disabled");

await user.clear(sshKeyTextarea);
expect(acceptButton).toHaveAttribute("disabled");

await user.type(passwordInput, "s3cr3t");
await user.type(sshKeyTextarea, "FAKE SSH KEY");
expect(acceptButton).not.toHaveAttribute("disabled");

// Request setting defined root method when Accept button is clicked
await user.click(acceptButton);
expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({
password: "s3cr3t",
encryptedPassword: false,
sshkey: "FAKE SSH KEY",
});

await user.clear(passwordInput);
await user.click(acceptButton);
expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({
sshkey: "FAKE SSH KEY",
});

await user.clear(sshKeyTextarea);
await user.type(passwordInput, "t0ps3cr3t");

// Request setting root password when Accept button is clicked
await user.click(acceptButton);
expect(mockRootUserMutation.mutateAsync).toHaveBeenCalledWith({
password: "t0ps3cr3t",
Expand Down
111 changes: 23 additions & 88 deletions web/src/components/users/RootAuthMethodsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,11 @@
*/

import React, { useRef, useState } from "react";
import {
Button,
FileUpload,
Flex,
Form,
FormGroup,
FormHelperText,
HelperText,
HelperTextItem,
} from "@patternfly/react-core";
import { Flex, Form, FormGroup } from "@patternfly/react-core";
import { useLocation, useNavigate } from "react-router-dom";
import { Center } from "~/components/layout";
import { Page, PasswordInput } from "~/components/core";
import { useRootUserMutation } from "~/queries/users";
import { RootUserChanges } from "~/types/users";
import { ROOT as PATHS } from "~/routes/paths";
import { isEmpty } from "~/utils";
import { _ } from "~/i18n";
Expand All @@ -55,45 +45,18 @@ function RootAuthMethodsPage() {
const location = useLocation();
const setRootUser = useRootUserMutation();
const [password, setPassword] = useState("");
const [sshKey, setSSHKey] = useState("");
const [isUploading, setIsUploading] = useState(false);

const startUploading = () => setIsUploading(true);
const stopUploading = () => setIsUploading(false);
const clearKey = () => setSSHKey("");

const isFormValid = !isEmpty(password) || !isEmpty(sshKey);
const uploadFile = () => document.getElementById("sshKey-browse-button").click();
const isFormValid = !isEmpty(password);

const accept = async (e: React.SyntheticEvent) => {
e.preventDefault();
if (isEmpty(password)) return;

const data: Partial<RootUserChanges> = {};

if (!isEmpty(password)) {
data.password = password;
data.encryptedPassword = false;
}

if (!isEmpty(sshKey)) {
data.sshkey = sshKey;
}

if (isEmpty(data)) return;

await setRootUser.mutateAsync(data);
await setRootUser.mutateAsync({ password, encryptedPassword: false });

navigate(location.state?.from || PATHS.root, { replace: true });
};

// TRANSLATORS: %s will be replaced by a link with the text "upload".
const [sshKeyStartHelperText, sshKeyEndHelperText] = _(
"Write, paste, drop, or %s a SSH public key file in the above textarea.",
).split("%s");
// TRANSLATORS: this "upload" is a commanding verb, in the %s place of
// "Write, paste, drop, or %s a SSH public key file in the above textarea."
const uploadLinkText = _("upload");

return (
<Page>
<Page.Content>
Expand All @@ -102,58 +65,30 @@ function RootAuthMethodsPage() {
headerLevel="h2"
title={_("Setup root user authentication")}
description={
<p className={textStyles.fontSizeXl}>
{_(
"You must define at least one authentication method for the root user. You can still edit them anytime before the installation.",
)}
</p>
<Flex direction={{ default: "column" }} gap={{ default: "gapSm" }}>
<p className={textStyles.fontSizeLg}>
{_("Provide a password to ensure administrative access to the system.")}
</p>
<p className={textStyles.fontSizeMd}>
{_(
"You can change it or select another authentication method in the 'Users' section before installing.",
)}
</p>
</Flex>
}
pfCardProps={{ isCompact: false }}
pfCardBodyProps={{ isFilled: true }}
>
<Form id="rootAuthMethods" onSubmit={accept}>
<Flex direction={{ default: "column" }} rowGap={{ default: "rowGapXl" }}>
<FormGroup fieldId="rootPassword" label={_("Password")}>
<PasswordInput
inputRef={passwordRef}
id="rootPassword"
value={password}
className={sizingStyles.wAuto}
onChange={(_, value) => setPassword(value)}
/>
</FormGroup>
<FormGroup fieldId="sshKey" label={_("SSH public key")}>
<FileUpload
id="sshKey"
value={sshKey}
type="text"
aria-label={_(
"Write, paste, or drop an SSH public key here. You can also upload it by using the link below.",
)}
// TRANSLATORS: push button label
browseButtonText={_("Upload")}
// TRANSLATORS: push button label, clears the related input field
clearButtonText={_("Clear")}
isLoading={isUploading}
onDataChange={(_, value) => setSSHKey(value)}
onTextChange={(_, value) => setSSHKey(value)}
onReadStarted={startUploading}
onReadFinished={stopUploading}
onClearClick={clearKey}
/>
<FormHelperText>
<HelperText>
<HelperTextItem variant="indeterminate">
{sshKeyStartHelperText}
<Button variant="link" isInline onClick={uploadFile}>
{uploadLinkText}
</Button>
{sshKeyEndHelperText}
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
</Flex>
<FormGroup fieldId="rootPassword" label={_("Password for root user")}>
<PasswordInput
inputRef={passwordRef}
id="rootPassword"
value={password}
className={sizingStyles.w_50OnMd}
onChange={(_, value) => setPassword(value)}
/>
</FormGroup>
</Form>
</Page.Section>
</Center>
Expand Down

0 comments on commit e24a29e

Please sign in to comment.