introduced password control (#27652)

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-03-19 14:23:32 +01:00 committed by GitHub
parent e501cfcfb3
commit 4e7c2a5fa3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 153 additions and 200 deletions

View file

@ -124,8 +124,10 @@ describe("Identity provider test", () => {
}
const instance = getSocialIdpClassInstance($idp.testName);
instance
.typeDisplayOrder("0")
.clickAdd()
.typeClientId("1")
.typeClientId("")
.typeClientSecret("1")
.typeClientSecret("")
.assertRequiredFieldsErrorsExist()
.fillData($idp.testName)
.clickAdd()
@ -139,10 +141,7 @@ describe("Identity provider test", () => {
createProviderPage.checkGitHubCardVisible().clickGitHubCard();
createProviderPage.checkAddButtonDisabled();
createProviderPage
.fill(identityProviderName)
.clickAdd()
.checkClientIdRequiredMessage(true);
createProviderPage.fill(identityProviderName).checkAddButtonDisabled();
createProviderPage.fill(identityProviderName, "123").clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
@ -298,9 +297,8 @@ describe("Identity provider test", () => {
createProviderPage.checkAddButtonDisabled();
createProviderPage
.fill(identityProviderName)
.clickAdd()
.checkClientIdRequiredMessage(true);
createProviderPage.fill(identityProviderName, "123").clickAdd();
.fill(identityProviderName, "123")
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg, true);
sidebarPage.goToIdentityProviders();

View file

@ -1,8 +1,8 @@
export default class CreateProviderPage {
#github = "github";
#clientIdField = "clientId";
#clientIdError = "#kc-client-secret-helper";
#clientSecretField = "clientSecret";
#clientIdError = "#config\\.clientSecret-helper";
#clientSecretField = "config.clientSecret";
#displayName = "displayName";
#discoveryEndpoint = "discoveryEndpoint";
#authorizationUrl = "config.authorizationUrl";

View file

@ -6,7 +6,7 @@ const masthead = new Masthead();
export default class ProviderBaseGeneralSettingsPage extends PageObject {
#redirectUriGroup = ".pf-c-clipboard-copy__group";
protected clientIdInput = "#kc-client-id";
protected clientSecretInput = "#kc-client-secret";
protected clientSecretInput = "config.clientSecret";
#displayOrderInput = "#kc-display-order";
#addBtn = "createProvider";
#cancelBtn = "cancel";
@ -22,12 +22,20 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
};
public typeClientId(clientId: string) {
cy.get(this.clientIdInput).type(clientId).blur();
if (clientId) {
cy.get(this.clientIdInput).type(clientId);
} else {
cy.get(this.clientIdInput).clear();
}
return this;
}
public typeClientSecret(clientSecret: string) {
cy.get(this.clientSecretInput).type(clientSecret).blur();
if (clientSecret) {
cy.findByTestId(this.clientSecretInput).type(clientSecret);
} else {
cy.findByTestId(this.clientSecretInput).clear();
}
return this;
}
@ -38,7 +46,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
}
public clickShowPassword() {
cy.get(this.clientSecretInput).parent().find("button").click();
cy.findByTestId(this.clientSecretInput).parent().find("button").click();
return this;
}
@ -68,12 +76,12 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
}
public assertClientSecretInputEqual(text: string) {
cy.get(this.clientSecretInput).should("have.text", text);
cy.findByTestId(this.clientSecretInput).should("have.text", text);
return this;
}
public assertDisplayOrderInputEqual(text: string) {
cy.get(this.clientSecretInput).should("have.text", text);
cy.findByTestId(this.clientSecretInput).should("have.text", text);
return this;
}
@ -124,7 +132,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
"have.value",
this.testData["ClientId"] + idpName,
);
cy.get(this.clientSecretInput).should("contain.value", "****");
cy.findByTestId(this.clientSecretInput).should("contain.value", "****");
cy.get(this.#displayOrderInput).should(
"have.value",
this.testData["DisplayOrder"],

View file

@ -18,7 +18,7 @@ export default class ProviderPage {
bindTypeInput = "#kc-bind-type";
#bindTypeList = "#kc-bind-type + ul";
bindDnInput = "config.bindDn.0";
bindCredsInput = "ldap-bind-credentials";
bindCredsInput = "config.bindCredential.0";
#testConnectionBtn = "test-connection-button";
#testAuthBtn = "test-auth-button";

View file

@ -222,7 +222,7 @@ export default class RealmSettingsPage extends CommonPage {
#eventListenersSaveBtn = "saveEventListenerBtn";
#eventListenersRevertBtn = "revertEventListenerBtn";
#eventListenersInputFld = ".pf-c-form-control.pf-c-select__toggle-typeahead";
#eventListenersDrpDwnOption = ".pf-c-select__menu-item";
#eventListenersDrpDwnOption = ".pf-c-select__menu";
#eventListenersDrwDwnSelect =
".pf-c-button.pf-c-select__toggle-button.pf-m-plain";
#eventListenerRemove = '[data-ouia-component-id="Remove"]';

View file

@ -10,11 +10,10 @@ import {
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { PasswordInput } from "ui-shared";
import { adminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { PasswordInput } from "../../components/password-input/PasswordInput";
import { useAccess } from "../../context/access/Access";
import useFormatDate from "../../utils/useFormatDate";
import { CopyToClipboardButton } from "../scopes/CopyToClipboardButton";

View file

@ -1,9 +1,5 @@
import type KeyStoreConfig from "@keycloak/keycloak-admin-client/lib/defs/keystoreConfig";
import { FormGroup } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem, TextControl } from "ui-shared";
import { PasswordInput } from "../../components/password-input/PasswordInput";
import { PasswordControl, TextControl } from "ui-shared";
export const StoreSettings = ({
hidePassword = false,
@ -13,10 +9,6 @@ export const StoreSettings = ({
isSaml?: boolean;
}) => {
const { t } = useTranslation();
const {
register,
formState: { errors },
} = useFormContext<KeyStoreConfig>();
return (
<>
@ -29,26 +21,14 @@ export const StoreSettings = ({
}}
/>
{!hidePassword && (
<FormGroup
<PasswordControl
name="keyPassword"
label={t("keyPassword")}
fieldId="keyPassword"
isRequired
labelIcon={
<HelpItem
helpText={t("keyPasswordHelp")}
fieldLabelId="keyPassword"
/>
}
helperTextInvalid={t("required")}
validated={errors.keyPassword ? "error" : "default"}
>
<PasswordInput
data-testid="keyPassword"
id="keyPassword"
validated={errors.keyPassword ? "error" : "default"}
{...register("keyPassword", { required: true })}
/>
</FormGroup>
labelIcon={t("keyPasswordHelp")}
rules={{
required: t("required"),
}}
/>
)}
{isSaml && (
<TextControl
@ -57,26 +37,14 @@ export const StoreSettings = ({
labelIcon={t("realmCertificateAliasHelp")}
/>
)}
<FormGroup
<PasswordControl
name="storePassword"
label={t("storePassword")}
fieldId="storePassword"
isRequired
labelIcon={
<HelpItem
helpText={t("storePasswordHelp")}
fieldLabelId="storePassword"
/>
}
helperTextInvalid={t("required")}
validated={errors.storePassword ? "error" : "default"}
>
<PasswordInput
data-testid="storePassword"
id="storePassword"
validated={errors.storePassword ? "error" : "default"}
{...register("storePassword", { required: true })}
/>
</FormGroup>
labelIcon={t("storePasswordHelp")}
rules={{
required: t("required"),
}}
/>
</>
);
};

View file

@ -1,11 +1,7 @@
import { FormGroup } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
import { PasswordInput } from "../password-input/PasswordInput";
import type { ComponentProps } from "./components";
import { PasswordControl } from "ui-shared";
import { convertToName } from "./DynamicComponents";
import type { ComponentProps } from "./components";
export const PasswordComponent = ({
name,
@ -16,22 +12,17 @@ export const PasswordComponent = ({
isDisabled = false,
}: ComponentProps) => {
const { t } = useTranslation();
const { register } = useFormContext();
return (
<FormGroup
<PasswordControl
name={convertToName(name!)}
label={t(label!)}
labelIcon={<HelpItem helpText={t(helpText!)} fieldLabelId={`${label}`} />}
fieldId={name!}
isRequired={required}
>
<PasswordInput
id={name!}
data-testid={name}
isDisabled={isDisabled}
defaultValue={defaultValue?.toString()}
{...register(convertToName(name!))}
/>
</FormGroup>
labelIcon={t(helpText!)}
isDisabled={isDisabled}
defaultValue={defaultValue?.toString()}
rules={{
required: { value: !!required, message: t("required") },
}}
/>
);
};

View file

@ -26,7 +26,7 @@ import { GeneralSettings } from "./GeneralSettings";
export default function AddIdentityProvider() {
const { t } = useTranslation();
const { providerId } = useParams<IdentityProviderCreateParams>();
const form = useForm<IdentityProviderRepresentation>();
const form = useForm<IdentityProviderRepresentation>({ mode: "onChange" });
const serverInfo = useServerInfo();
const providerInfo = useMemo(() => {
@ -48,7 +48,7 @@ export default function AddIdentityProvider() {
const {
handleSubmit,
formState: { isDirty },
formState: { isValid },
} = form;
const { addAlert, addError } = useAlerts();
@ -100,7 +100,7 @@ export default function AddIdentityProvider() {
</FormProvider>
<ActionGroup>
<Button
isDisabled={!isDirty}
isDisabled={!isValid}
variant="primary"
type="submit"
data-testid="createProvider"

View file

@ -2,10 +2,8 @@ import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/
import { FormGroup, ValidatedOptions } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
import { HelpItem, PasswordControl } from "ui-shared";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { PasswordInput } from "../../components/password-input/PasswordInput";
export const ClientIdSecret = ({
secretRequired = true,
@ -44,40 +42,13 @@ export const ClientIdSecret = ({
{...register("config.clientId", { required: true })}
/>
</FormGroup>
<FormGroup
<PasswordControl
name="config.clientSecret"
label={t("clientSecret")}
labelIcon={
<HelpItem
helpText={t("clientSecretHelp")}
fieldLabelId="clientSecret"
/>
}
fieldId="kc-client-secret"
isRequired={secretRequired}
validated={
errors.config?.clientSecret
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("required")}
>
{create ? (
<PasswordInput
isRequired={secretRequired}
id="kc-client-secret"
data-testid="clientSecret"
{...register("config.clientSecret", { required: secretRequired })}
/>
) : (
<KeycloakTextInput
isRequired={secretRequired}
type="password"
id="kc-client-secret"
data-testid="clientSecret"
{...register("config.clientSecret", { required: secretRequired })}
/>
)}
</FormGroup>
labelIcon={t("clientSecretHelp")}
hasReveal={create}
rules={{ required: { value: secretRequired, message: t("required") } }}
/>
</>
);
};

View file

@ -13,16 +13,21 @@ import {
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { FormPanel, HelpItem, SwitchControl, TextControl } from "ui-shared";
import {
FormPanel,
SwitchControl,
TextControl,
PasswordControl,
} from "ui-shared";
import { adminClient } from "../admin-client";
import { useAlerts } from "../components/alert/Alerts";
import { FormAccess } from "../components/form/FormAccess";
import { PasswordInput } from "../components/password-input/PasswordInput";
import { useRealm } from "../context/realm-context/RealmContext";
import { toUser } from "../user/routes/User";
import { emailRegexPattern } from "../util";
import { useCurrentUser } from "../utils/useCurrentUser";
import useToggle from "../utils/useToggle";
import "./realm-settings-section.css";
type RealmSettingsEmailTabProps = {
@ -42,15 +47,7 @@ export const RealmSettingsEmailTab = ({
const currentUser = useCurrentUser();
const form = useForm<FormFields>({ defaultValues: realm });
const {
register,
control,
handleSubmit,
watch,
reset: resetForm,
getValues,
formState: { errors },
} = form;
const { control, handleSubmit, watch, reset: resetForm, getValues } = form;
const reset = () => resetForm(realm);
const watchFromValue = watch("smtpServer.from", "");
@ -219,29 +216,14 @@ export const RealmSettingsEmailTab = ({
required: t("required"),
}}
/>
<FormGroup
<PasswordControl
name="smtpServer.password"
label={t("password")}
fieldId="kc-username"
isRequired
validated={errors.smtpServer?.password ? "error" : "default"}
helperTextInvalid={t("required")}
labelIcon={
<HelpItem
helpText={t("passwordHelp")}
fieldLabelId="password"
/>
}
>
<PasswordInput
id="kc-password"
data-testid="password-input"
aria-label={t("password")}
validated={
errors.smtpServer?.password ? "error" : "default"
}
{...register("smtpServer.password", { required: true })}
/>
</FormGroup>
labelIcon={t("passwordHelp")}
rules={{
required: t("required"),
}}
/>
</>
)}
{currentUser && (

View file

@ -7,7 +7,6 @@ import {
SelectOption,
SelectVariant,
Switch,
ValidatedOptions,
} from "@patternfly/react-core";
import { get, isEqual } from "lodash-es";
import { useState } from "react";
@ -18,11 +17,10 @@ import {
useWatch,
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem, TextControl } from "ui-shared";
import { HelpItem, PasswordControl, TextControl } from "ui-shared";
import { adminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form/FormAccess";
import { PasswordInput } from "../../components/password-input/PasswordInput";
import { WizardSectionHeader } from "../../components/wizard-section-header/WizardSectionHeader";
import { useRealm } from "../../context/realm-context/RealmContext";
@ -264,38 +262,15 @@ export const LdapSettingsConnection = ({
required: t("validateBindDn"),
}}
/>
<FormGroup
<PasswordControl
name="config.bindCredential.0"
label={t("bindCredentials")}
labelIcon={
<HelpItem
helpText={t("bindCredentialsHelp")}
fieldLabelId="bindCredentials"
/>
}
fieldId="kc-ui-bind-credentials"
helperTextInvalid={t("validateBindCredentials")}
validated={
(form.formState.errors.config as any)?.bindCredential
? ValidatedOptions.error
: ValidatedOptions.default
}
isRequired
>
<PasswordInput
hasReveal={!edit}
isRequired
id="kc-ui-bind-credentials"
data-testid="ldap-bind-credentials"
validated={
(form.formState.errors.config as any)?.bindCredential
? ValidatedOptions.error
: ValidatedOptions.default
}
{...form.register("config.bindCredential.0", {
required: true,
})}
/>
</FormGroup>
labelIcon={t("bindCredentialsHelp")}
hasReveal={!edit}
rules={{
required: t("validateBindCredentials"),
}}
/>
</>
)}
<FormGroup fieldId="kc-test-auth-button">

View file

@ -9,6 +9,7 @@ import {
} from "@patternfly/react-core";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { PasswordInput } from "ui-shared";
import { adminClient } from "../../admin-client";
import { DefaultSwitchControl } from "../../components/SwitchControl";
import { useAlerts } from "../../components/alert/Alerts";
@ -16,7 +17,6 @@ import {
ConfirmDialogModal,
useConfirmDialog,
} from "../../components/confirm-dialog/ConfirmDialog";
import { PasswordInput } from "../../components/password-input/PasswordInput";
import useToggle from "../../utils/useToggle";
type ResetPasswordDialogProps = {

View file

@ -0,0 +1,60 @@
import { ValidatedOptions } from "@patternfly/react-core";
import {
FieldPath,
FieldValues,
PathValue,
UseControllerProps,
useController,
} from "react-hook-form";
import { FormLabel } from "./FormLabel";
import { PasswordInput, PasswordInputProps } from "./PasswordInput";
export type PasswordControlProps<
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>,
> = UseControllerProps<T, P> &
Omit<PasswordInputProps, "name" | "isRequired" | "required"> & {
label: string;
labelIcon?: string;
isDisabled?: boolean;
helperText?: string;
};
export const PasswordControl = <
T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>,
>(
props: PasswordControlProps<T, P>,
) => {
const { labelIcon, ...rest } = props;
const required = !!props.rules?.required;
const defaultValue = props.defaultValue ?? ("" as PathValue<T, P>);
const { field, fieldState } = useController({
...props,
defaultValue,
});
return (
<FormLabel
name={props.name}
label={props.label}
labelIcon={labelIcon}
isRequired={required}
error={fieldState.error}
helperText={props.helperText}
>
<PasswordInput
isRequired={required}
id={props.name}
data-testid={props.name}
validated={
fieldState.error ? ValidatedOptions.error : ValidatedOptions.default
}
isDisabled={props.isDisabled}
{...rest}
{...field}
/>
</FormLabel>
);
};

View file

@ -1,14 +1,13 @@
import { forwardRef, MutableRefObject, Ref, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, InputGroup } from "@patternfly/react-core";
import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
import { forwardRef, MutableRefObject, Ref, useState } from "react";
import { useTranslation } from "react-i18next";
import {
KeycloakTextInput,
KeycloakTextInputProps,
} from "../keycloak-text-input/KeycloakTextInput";
type PasswordInputProps = KeycloakTextInputProps & {
export type PasswordInputProps = KeycloakTextInputProps & {
hasReveal?: boolean;
};

View file

@ -28,3 +28,5 @@ export {
export type { UserFormFields } from "./user-profile/utils";
export { ScrollForm, mainPageContentId } from "./scroll-form/ScrollForm";
export { FormPanel } from "./scroll-form/FormPanel";
export { PasswordControl } from "./controls/PasswordControl";
export { PasswordInput } from "./controls/PasswordInput";