added keys tab to client details (#557)

This commit is contained in:
Erik Jan de Wit 2021-05-04 10:11:58 +02:00 committed by GitHub
parent 15677b6bfb
commit c2b78c471b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 787 additions and 37 deletions

1
.gitignore vendored
View file

@ -141,3 +141,4 @@ server/
########### ###########
**/assets **/assets
**/cypress.env.json **/cypress.env.json
cypress/downloads/

View file

@ -9,6 +9,7 @@ import AdminClient from "../support/util/AdminClient";
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab"; import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
import { keycloakBefore } from "../support/util/keycloak_before"; import { keycloakBefore } from "../support/util/keycloak_before";
import RoleMappingTab from "../support/pages/admin_console/manage/RoleMappingTab"; import RoleMappingTab from "../support/pages/admin_console/manage/RoleMappingTab";
import KeysTab from "../support/pages/admin_console/manage/clients/KeysTab";
let itemId = "client_crud"; let itemId = "client_crud";
const loginPage = new LoginPage(); const loginPage = new LoginPage();
@ -198,4 +199,43 @@ describe("Clients test", function () {
.checkRoles(["manage-account", "offline_access", "uma_authorization"]); .checkRoles(["manage-account", "offline_access", "uma_authorization"]);
}); });
}); });
describe("Keys tab test", () => {
const keysName = "keys-client";
beforeEach(() => {
keycloakBefore();
loginPage.logIn();
sidebarPage.goToClients();
listingPage.searchItem(keysName).goToItemDetails(keysName);
});
before(() => {
new AdminClient().createClient({
protocol: "openid-connect",
clientId: keysName,
publicClient: false,
});
});
after(() => {
new AdminClient().deleteClient(keysName);
});
it("change use JWKS Url", () => {
const keysTab = new KeysTab();
keysTab.goToTab().checkSaveDisabled();
keysTab.toggleUseJwksUrl().checkSaveDisabled(false);
});
it("generate new keys", () => {
const keysTab = new KeysTab();
keysTab.goToTab().clickGenerate();
keysTab.fillGenerateModal("keyname", "123", "1234").clickConfirm();
masthead.checkNotificationMessage(
"New key pair and certificate generated successfully"
);
});
});
}); });

View file

@ -9,13 +9,13 @@ export default class ListingPage {
importBtn: string; importBtn: string;
constructor() { constructor() {
this.searchInput = '.pf-c-toolbar__item [type="search"]'; this.searchInput = '.pf-c-toolbar__item [type="search"]:visible';
this.itemsRows = "table"; this.itemsRows = "table";
this.itemRowDrpDwn = ".pf-c-dropdown > button"; this.itemRowDrpDwn = ".pf-c-dropdown > button";
this.exportBtn = '[role="menuitem"]:nth-child(1)'; this.exportBtn = '[role="menuitem"]:nth-child(1)';
this.deleteBtn = '[role="menuitem"]:nth-child(2)'; this.deleteBtn = '[role="menuitem"]:nth-child(2)';
this.searchBtn = this.searchBtn =
".pf-c-page__main .pf-c-toolbar__content-section button.pf-m-control"; ".pf-c-page__main .pf-c-toolbar__content-section button.pf-m-control:visible";
this.createBtn = this.createBtn =
".pf-c-page__main .pf-c-toolbar__content-section button.pf-m-primary"; ".pf-c-page__main .pf-c-toolbar__content-section button.pf-m-primary";
this.importBtn = this.importBtn =

View file

@ -0,0 +1,49 @@
export default class KeysTab {
private tabName = "#pf-tab-keys-keys";
private useJwksUrl = "useJwksUrl";
private saveKeys = "saveKeys";
private generate = "generate";
private keyAlias = "keyAlias";
private keyPassword = "keyPassword";
private storePassword = "storePassword";
private confirm = "confirm";
goToTab() {
cy.get(this.tabName).click();
return this;
}
checkSaveDisabled(disabled = true) {
cy.getId(this.saveKeys).should((!disabled ? "not." : "") + "be.disabled");
return this;
}
toggleUseJwksUrl() {
cy.getId(this.useJwksUrl).click({ force: true });
return this;
}
clickGenerate() {
cy.getId(this.generate).click();
return this;
}
clickConfirm() {
cy.getId(this.confirm).click();
return this;
}
fillGenerateModal(
keyAlias: string,
keyPassword: string,
storePassword: string
) {
cy.getId(this.keyAlias)
.type(keyAlias)
.getId(this.keyPassword)
.type(keyPassword)
.getId(this.storePassword)
.type(storePassword);
return this;
}
}

View file

@ -27,7 +27,7 @@
"@patternfly/react-table": "4.26.7", "@patternfly/react-table": "4.26.7",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^19.6.2", "i18next": "^19.6.2",
"keycloak-admin": "1.14.11", "keycloak-admin": "1.14.15",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"react": "^16.8.5", "react": "^16.8.5",

View file

@ -45,6 +45,7 @@ import { ServiceAccount } from "./service-account/ServiceAccount";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { AdvancedTab } from "./AdvancedTab"; import { AdvancedTab } from "./AdvancedTab";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { Keys } from "./keys/Keys";
type ClientDetailHeaderProps = { type ClientDetailHeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -290,6 +291,15 @@ export const ClientDetails = () => {
reset={() => setupForm(client)} reset={() => setupForm(client)}
/> />
</Tab> </Tab>
{!client.publicClient && (
<Tab
id="keys"
eventKey="keys"
title={<TabTitleText>{t("keys")}</TabTitleText>}
>
<Keys clientId={clientId} save={() => save()} />
</Tab>
)}
{!client.publicClient && ( {!client.publicClient && (
<Tab <Tab
id="credentials" id="credentials"

View file

@ -59,6 +59,14 @@
"logoutServiceRedirectBindingURL": "SAML Redirect Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.", "logoutServiceRedirectBindingURL": "SAML Redirect Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.",
"authenticationOverrides": "Override realm authentication flow bindings.", "authenticationOverrides": "Override realm authentication flow bindings.",
"browserFlow": "Select the flow you want to use for browser authentication.", "browserFlow": "Select the flow you want to use for browser authentication.",
"directGrant": "Select the flow you want to use for direct grant authentication." "directGrant": "Select the flow you want to use for direct grant authentication.",
"useJwksUrl": "If the switch is on, client public keys will be downloaded from given JWKS URL. This allows great flexibility because new keys will be always re-downloaded again when client generates new keypair. If the switch is off, public key (or certificate) from the Keycloak DB is used, so when client keypair changes, you always need to import new key (or certificate) to the Keycloak DB as well.",
"certificate": "Client Certificate for validate JWT issued by client and signed by Client private key from your keystore.",
"jwksUrl": "URL where client keys in JWK format are stored. See JWK specification for more details. If you use Keycloak client adapter with \"jwt\" credential, you can use URL of your app with '/k_jwks' suffix. For example 'http://www.myhost.com/myapp/k_jwks' .",
"generateKeysDescription": "If you generate new keys, you can download the keystore with the private key automatically and save it on your client's side. Keycloak server will save just the certificate and public key, but not the private key.",
"archiveFormat": "Java keystore or PKCS12 archive format.",
"keyAlias": "Archive alias for your private key and certificate.",
"keyPassword": "Password to access the private key in the archive",
"storePassword": "Password to access the archive itself"
} }
} }

View file

@ -0,0 +1,118 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import {
Button,
ButtonVariant,
Form,
FormGroup,
Modal,
ModalVariant,
Select,
SelectOption,
SelectVariant,
Text,
TextContent,
TextInput,
} from "@patternfly/react-core";
import KeyStoreConfig from "keycloak-admin/lib/defs/keystoreConfig";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { PasswordInput } from "../../components/password-input/PasswordInput";
import { StoreSettings } from "./StoreSettings";
type GenerateKeyDialogProps = {
toggleDialog: () => void;
save: (keyStoreConfig: KeyStoreConfig) => void;
};
export const GenerateKeyDialog = ({
save,
toggleDialog,
}: GenerateKeyDialogProps) => {
const { t } = useTranslation("clients");
const { register, control, handleSubmit } = useForm<KeyStoreConfig>();
const [openArchiveFormat, setOpenArchiveFormat] = useState(false);
return (
<Modal
variant={ModalVariant.medium}
title={t("generateKeys")}
isOpen
onClose={toggleDialog}
actions={[
<Button
id="modal-confirm"
key="confirm"
data-testid="confirm"
onClick={() => {
handleSubmit((config) => {
save(config);
toggleDialog();
})();
}}
>
{t("generate")}
</Button>,
<Button
id="modal-cancel"
key="cancel"
data-testid="cancel"
variant={ButtonVariant.link}
onClick={() => {
toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<TextContent>
<Text>{t("clients-help:generateKeysDescription")}</Text>
</TextContent>
<Form className="pf-u-pt-lg">
<FormGroup
label={t("archiveFormat")}
labelIcon={
<HelpItem
helpText="clients-help:archiveFormat"
forLabel={t("archiveFormat")}
forID="archiveFormat"
/>
}
fieldId="archiveFormat"
>
<Controller
name="format"
defaultValue="JKS"
control={control}
render={({ onChange, value }) => (
<Select
toggleId="archiveFormat"
onToggle={() => setOpenArchiveFormat(!openArchiveFormat)}
onSelect={(_, value) => {
onChange(value as string);
setOpenArchiveFormat(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("archiveFormat")}
isOpen={openArchiveFormat}
>
{["JKS", "PKCS12"].map((option) => (
<SelectOption
selected={option === value}
key={option}
value={option}
/>
))}
</Select>
)}
/>
</FormGroup>
<StoreSettings register={register} />
</Form>
</Modal>
);
};

View file

@ -0,0 +1,150 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useForm, useWatch } from "react-hook-form";
import {
Button,
ButtonVariant,
FileUpload,
Form,
FormGroup,
Modal,
ModalVariant,
Select,
SelectOption,
SelectVariant,
Text,
TextContent,
} from "@patternfly/react-core";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { StoreSettings } from "./StoreSettings";
type ImportKeyDialogProps = {
toggleDialog: () => void;
save: (importFile: ImportFile) => void;
};
const baseFormats = ["JKS", "PKCS12"];
const formats = baseFormats.concat([
"Certificate PEM",
"Public Key PEM",
"JSON Web Key Set",
]);
export type ImportFile = {
keystoreFormat: string;
keyAlias: string;
storePassword: string;
file: { value: File; filename: string };
};
export const ImportKeyDialog = ({
save,
toggleDialog,
}: ImportKeyDialogProps) => {
const { t } = useTranslation("clients");
const { register, control, handleSubmit } = useForm<ImportFile>();
const [openArchiveFormat, setOpenArchiveFormat] = useState(false);
const format = useWatch<string>({
control,
name: "keystoreFormat",
defaultValue: "JKS",
});
return (
<Modal
variant={ModalVariant.medium}
title={t("generateKeys")}
isOpen
onClose={toggleDialog}
actions={[
<Button
id="modal-confirm"
key="confirm"
onClick={() => {
handleSubmit((importFile) => {
save(importFile);
toggleDialog();
})();
}}
>
{t("import")}
</Button>,
<Button
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]}
>
<TextContent>
<Text>{t("clients-help:generateKeysDescription")}</Text>
</TextContent>
<Form className="pf-u-pt-lg">
<FormGroup
label={t("archiveFormat")}
labelIcon={
<HelpItem
helpText="clients-help:archiveFormat"
forLabel={t("archiveFormat")}
forID="archiveFormat"
/>
}
fieldId="archiveFormat"
>
<Controller
name="keystoreFormat"
control={control}
defaultValue="JKS"
render={({ onChange, value }) => (
<Select
toggleId="archiveFormat"
onToggle={() => setOpenArchiveFormat(!openArchiveFormat)}
onSelect={(_, value) => {
onChange(value as string);
setOpenArchiveFormat(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("archiveFormat")}
isOpen={openArchiveFormat}
>
{formats.map((option) => (
<SelectOption
selected={option === value}
key={option}
value={option}
/>
))}
</Select>
)}
/>
</FormGroup>
{baseFormats.includes(format) && (
<StoreSettings register={register} hidePassword />
)}
<FormGroup label={t("importFile")} fieldId="importFile">
<Controller
name="file"
control={control}
defaultValue=""
render={({ onChange, value }) => (
<FileUpload
id="importFile"
value={value.value}
filename={value.filename}
onChange={(value, filename) => onChange({ value, filename })}
/>
)}
/>
</FormGroup>
</Form>
</Modal>
);
};

249
src/clients/keys/Keys.tsx Normal file
View file

@ -0,0 +1,249 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import FileSaver from "file-saver";
import {
ActionGroup,
AlertVariant,
Button,
Card,
CardBody,
CardHeader,
CardTitle,
FormGroup,
PageSection,
Switch,
Text,
TextArea,
TextContent,
TextInput,
} from "@patternfly/react-core";
import CertificateRepresentation from "keycloak-admin/lib/defs/certificateRepresentation";
import KeyStoreConfig from "keycloak-admin/lib/defs/keystoreConfig";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { FormAccess } from "../../components/form-access/FormAccess";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { ClientForm } from "../ClientDetails";
import { GenerateKeyDialog } from "./GenerateKeyDialog";
import {
asyncStateFetch,
useAdminClient,
} from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import { ImportKeyDialog, ImportFile } from "./ImportKeyDialog";
import { useErrorHandler } from "react-error-boundary";
type KeysProps = {
save: () => void;
clientId: string;
};
const attr = "jwt.credential";
export const Keys = ({ clientId, save }: KeysProps) => {
const { t } = useTranslation("clients");
const {
control,
register,
formState: { isDirty },
} = useFormContext<ClientForm>();
const adminClient = useAdminClient();
const errorHandler = useErrorHandler();
const { addAlert } = useAlerts();
const [keyInfo, setKeyInfo] = useState<CertificateRepresentation>();
const [openGenerateKeys, setOpenGenerateKeys] = useState(false);
const [openImportKeys, setOpenImportKeys] = useState(false);
const useJwksUrl = useWatch({
control,
name: "attributes.use-jwks-url",
defaultValue: "false",
});
useEffect(
() =>
asyncStateFetch(
() => adminClient.clients.getKeyInfo({ id: clientId, attr }),
(info) => setKeyInfo(info),
errorHandler
),
[]
);
const generate = async (config: KeyStoreConfig) => {
try {
const keyStore = await adminClient.clients.generateAndDownloadKey(
{
id: clientId,
attr,
},
config
);
FileSaver.saveAs(
new Blob([keyStore], { type: "application/octet-stream" }),
`keystore.${config.format == "PKCS12" ? "p12" : "jks"}`
);
addAlert(t("generateSuccess"), AlertVariant.success);
} catch (error) {
addAlert(
t("generateError", {
error: error.response?.data?.errorMessage || error,
}),
AlertVariant.danger
);
}
};
const importKey = async (importFile: ImportFile) => {
try {
const formData = new FormData();
const { file, ...rest } = importFile;
Object.entries(rest).map((entry) =>
formData.append(entry[0], entry[1] as string)
);
formData.append("file", file.value);
await adminClient.clients.uploadCertificate(
{ id: clientId, attr },
formData
);
addAlert(t("importSuccess"), AlertVariant.success);
} catch (error) {
addAlert(
t("importError", {
error: error.response?.data?.errorMessage || error,
}),
AlertVariant.danger
);
}
};
return (
<PageSection variant="light" className="keycloak__form">
{openGenerateKeys && (
<GenerateKeyDialog
toggleDialog={() => setOpenGenerateKeys(!openGenerateKeys)}
save={generate}
/>
)}
{openImportKeys && (
<ImportKeyDialog
toggleDialog={() => setOpenImportKeys(!openImportKeys)}
save={importKey}
/>
)}
<Card isFlat>
<CardHeader>
<CardTitle>{t("jwksUrlConfig")}</CardTitle>
</CardHeader>
<CardBody>
<TextContent>
<Text>{t("keysIntro")}</Text>
</TextContent>
</CardBody>
<CardBody>
<FormAccess role="manage-clients" isHorizontal>
<FormGroup
hasNoPaddingTop
label={t("useJwksUrl")}
fieldId="useJwksUrl"
labelIcon={
<HelpItem
helpText="clients-help:useJwksUrl"
forLabel={t("useJwksUrl")}
forID="useJwksUrl"
/>
}
>
<Controller
name="attributes.use-jwks-url"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
data-testid="useJwksUrl"
id="useJwksUrl"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(`${value}`)}
/>
)}
/>
</FormGroup>
{useJwksUrl !== "true" && (
<>
{keyInfo ? (
<FormGroup
label={t("certificate")}
fieldId="certificate"
labelIcon={
<HelpItem
helpText="clients-help:certificate"
forLabel={t("certificate")}
forID="certificate"
/>
}
>
<TextArea
readOnly
rows={5}
id="certificate"
value={keyInfo.certificate}
/>
</FormGroup>
) : (
"No client certificate configured"
)}
</>
)}
{useJwksUrl === "true" && (
<FormGroup
label={t("jwksUrl")}
fieldId="jwksUrl"
labelIcon={
<HelpItem
helpText="clients-help:jwksUrl"
forLabel={t("jwksUrl")}
forID="jwksUrl"
/>
}
>
<TextInput
type="text"
id="jwksUrl"
name="attributes.jwks-url"
ref={register}
/>
</FormGroup>
)}
<ActionGroup>
<Button
data-testid="saveKeys"
onClick={save}
isDisabled={!isDirty}
>
{t("common:save")}
</Button>
<Button
data-testid="generate"
variant="secondary"
onClick={() => setOpenGenerateKeys(true)}
>
{t("generateNewKeys")}
</Button>
<Button
data-testid="import"
variant="secondary"
onClick={() => setOpenImportKeys(true)}
isDisabled={useJwksUrl === "true"}
>
{t("import")}
</Button>
</ActionGroup>
</FormAccess>
</CardBody>
</Card>
</PageSection>
);
};

View file

@ -0,0 +1,78 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { FormGroup, TextInput } from "@patternfly/react-core";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { PasswordInput } from "../../components/password-input/PasswordInput";
export const StoreSettings = ({
register,
hidePassword = false,
}: {
register: () => void;
hidePassword?: boolean;
}) => {
const { t } = useTranslation("clients");
return (
<>
<FormGroup
label={t("keyAlias")}
fieldId="keyAlias"
labelIcon={
<HelpItem
helpText="clients-help:keyAlias"
forLabel={t("keyAlias")}
forID="keyAlias"
/>
}
>
<TextInput
data-testid="keyAlias"
type="text"
id="keyAlias"
name="keyAlias"
ref={register}
/>
</FormGroup>
{!hidePassword && (
<FormGroup
label={t("keyPassword")}
fieldId="keyPassword"
labelIcon={
<HelpItem
helpText="clients-help:keyPassword"
forLabel={t("keyPassword")}
forID="keyPassword"
/>
}
>
<PasswordInput
data-testid="keyPassword"
id="keyPassword"
name="keyPassword"
ref={register}
/>
</FormGroup>
)}
<FormGroup
label={t("storePassword")}
fieldId="storePassword"
labelIcon={
<HelpItem
helpText="clients-help:storePassword"
forLabel={t("storePassword")}
forID="storePassword"
/>
}
>
<PasswordInput
data-testid="storePassword"
id="storePassword"
name="storePassword"
ref={register}
/>
</FormGroup>
</>
);
};

View file

@ -13,6 +13,7 @@
"encryptAssertions": "Encrypt assertions", "encryptAssertions": "Encrypt assertions",
"clientSignature": "Client signature required", "clientSignature": "Client signature required",
"downloadAdaptorTitle": "Download adaptor configs", "downloadAdaptorTitle": "Download adaptor configs",
"keys": "Keys",
"credentials": "Credentials", "credentials": "Credentials",
"roles": "Roles", "roles": "Roles",
"createRole": "Create role", "createRole": "Create role",
@ -188,7 +189,7 @@
"openIdConnectCompatibilityModes": "Open ID Connect Compatibly Modes", "openIdConnectCompatibilityModes": "Open ID Connect Compatibly Modes",
"excludeSessionStateFromAuthenticationResponse": "Exclude Session State From Authentication Response", "excludeSessionStateFromAuthenticationResponse": "Exclude Session State From Authentication Response",
"assertionConsumerServicePostBindingURL": "Assertion Consumer Service POST Binding URL", "assertionConsumerServicePostBindingURL": "Assertion Consumer Service POST Binding URL",
"assertionConsumerServiceRedirectBindingURL" :"Assertion Consumer Service Redirect Binding URL", "assertionConsumerServiceRedirectBindingURL": "Assertion Consumer Service Redirect Binding URL",
"logoutServicePostBindingURL": "Logout Service POST Binding URL", "logoutServicePostBindingURL": "Logout Service POST Binding URL",
"logoutServiceRedirectBindingURL": "Logout Service Redirect Binding URL", "logoutServiceRedirectBindingURL": "Logout Service Redirect Binding URL",
"advancedSettings": "Advanced Settings", "advancedSettings": "Advanced Settings",
@ -198,7 +199,24 @@
"keyForCodeExchange": "Proof Key for Code Exchange Code Challenge Method", "keyForCodeExchange": "Proof Key for Code Exchange Code Challenge Method",
"authenticationOverrides": "Authentication flow overrides", "authenticationOverrides": "Authentication flow overrides",
"browserFlow": "Browser Flow", "browserFlow": "Browser Flow",
"directGrant": "Direct Grant Flow" "directGrant": "Direct Grant Flow",
"jwksUrlConfig": "JWKS URL configs",
"keysIntro": "If \"Use JWKS URL switch\" is on, you need to fill a valid JWKS URL. After saving, admin can download keys from the JWKS URL or keys will be downloaded automatically by Keycloak server when see the stuff signed by the unknown KID",
"useJwksUrl": "Use JWKS URL",
"certificate": "Certificate",
"jwksUrl": "JWKS URL",
"generateNewKeys": "Generate new keys",
"generateKeys": "Generate keys?",
"generate": "Generate",
"archiveFormat": "Archive format",
"keyAlias": "Key alias",
"keyPassword": "Key password",
"storePassword": "Store password",
"generateSuccess": "New key pair and certificate generated successfully",
"generateError": "Could not generate new key pair and certificate {{error}}",
"import": "Import",
"importFile": "Import file",
"importSuccess": "New certificate imported",
"importError": "Could not import certificate {{error}}"
} }
} }

View file

@ -1,5 +1,6 @@
{ {
"common-help": { "common-help": {
"helpToggleInfo": "This toggle will enable / disable part of the help info in the console. Includes any help text, links and popovers." "helpToggleInfo": "This toggle will enable / disable part of the help info in the console. Includes any help text, links and popovers.",
"showPassword": "Show password field in clear text"
} }
} }

View file

@ -0,0 +1,40 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
InputGroup,
TextInput,
TextInputProps,
} from "@patternfly/react-core";
import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
const PasswordInputBase = ({ innerRef, ...rest }: TextInputProps) => {
const { t } = useTranslation("common-help");
const [hidePassword, setHidePassword] = useState(true);
return (
<InputGroup>
<TextInput
{...rest}
type={hidePassword ? "password" : "text"}
ref={innerRef}
/>
<Button
variant="control"
aria-label={t("showPassword")}
onClick={() => setHidePassword(!hidePassword)}
>
{hidePassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</InputGroup>
);
};
export const PasswordInput = React.forwardRef(
(props: TextInputProps, ref: React.Ref<HTMLInputElement>) => (
<PasswordInputBase
{...props}
innerRef={ref as React.MutableRefObject<any>}
/>
)
);
PasswordInput.displayName = "PasswordInput";

View file

@ -1,7 +1,6 @@
import { import {
Button, Button,
FormGroup, FormGroup,
InputGroup,
Select, Select,
SelectOption, SelectOption,
SelectVariant, SelectVariant,
@ -12,9 +11,9 @@ import { useTranslation } from "react-i18next";
import React, { useState } from "react"; import React, { useState } from "react";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
import { Controller, UseFormMethods } from "react-hook-form"; import { Controller, UseFormMethods } from "react-hook-form";
import { EyeIcon, EyeSlashIcon } from "@patternfly/react-icons";
import { FormAccess } from "../../components/form-access/FormAccess"; import { FormAccess } from "../../components/form-access/FormAccess";
import { WizardSectionHeader } from "../../components/wizard-section-header/WizardSectionHeader"; import { WizardSectionHeader } from "../../components/wizard-section-header/WizardSectionHeader";
import { PasswordInput } from "../../components/password-input/PasswordInput";
export type LdapSettingsConnectionProps = { export type LdapSettingsConnectionProps = {
form: UseFormMethods; form: UseFormMethods;
@ -36,7 +35,6 @@ export const LdapSettingsConnection = ({
] = useState(false); ] = useState(false);
const [isBindTypeDropdownOpen, setIsBindTypeDropdownOpen] = useState(false); const [isBindTypeDropdownOpen, setIsBindTypeDropdownOpen] = useState(false);
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
return ( return (
<> <>
@ -283,28 +281,18 @@ export const LdapSettingsConnection = ({
fieldId="kc-console-bind-credentials" fieldId="kc-console-bind-credentials"
isRequired isRequired
> >
<InputGroup> <PasswordInput
<TextInput isRequired
isRequired id="kc-console-bind-credentials"
type={isPasswordVisible ? "text" : "password"} data-testid="ldap-bind-credentials"
id="kc-console-bind-credentials" name="config.bindCredential[0]"
data-testid="ldap-bind-credentials" ref={form.register({
name="config.bindCredential[0]" required: {
ref={form.register({ value: true,
required: { message: `${t("validateBindCredentials")}`,
value: true, },
message: `${t("validateBindCredentials")}`, })}
}, />
})}
/>
<Button
variant="control"
aria-label="show password button for bind credentials"
onClick={() => setIsPasswordVisible(!isPasswordVisible)}
>
{!isPasswordVisible ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</InputGroup>
{form.errors.config && {form.errors.config &&
form.errors.config.bindCredential && form.errors.config.bindCredential &&
form.errors.config.bindCredential[0] && ( form.errors.config.bindCredential[0] && (

View file

@ -15193,10 +15193,10 @@ junk@^3.1.0:
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
keycloak-admin@1.14.11: keycloak-admin@1.14.15:
version "1.14.11" version "1.14.15"
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.11.tgz#71415395eeb014f5a8675c951b23596ba33b6f35" resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.15.tgz#07d7433f69b802c94db9b4a9a36e7b968ca34c73"
integrity sha512-s0NNLdJ27oAx52pXsvJgm8O/KDb0dbPsnbc+f4uTaz/Gzh6QN6GJPCgAYJEZj/Re+oOm+OVRHTx8bhhlrom5hA== integrity sha512-P5TPweX6o4KO2RcBNPIbT2uNlzN7eXxAU+jo2Zg6QZRWXlO8xa2mlZbvcBBk0Smu58Z6bXNxFqAs3sqwzEcPIQ==
dependencies: dependencies:
axios "^0.21.0" axios "^0.21.0"
camelize "^1.0.0" camelize "^1.0.0"