Added extra step to OIDC client wizzard (#4035)

This commit is contained in:
Erik Jan de Wit 2023-01-17 15:29:42 +01:00 committed by GitHub
parent 158f471bea
commit 2e174cd4e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 243 additions and 199 deletions

View file

@ -262,6 +262,7 @@ describe("Clients test", () => {
.fillClientData(clientId) .fillClientData(clientId)
.continue() .continue()
.checkCapabilityConfigElements() .checkCapabilityConfigElements()
.continue()
.save(); .save();
commonPage commonPage
@ -304,7 +305,7 @@ describe("Clients test", () => {
.continue() .continue()
.checkClientIdRequiredMessage(); .checkClientIdRequiredMessage();
createClientPage.fillClientData("account").continue().save(); createClientPage.fillClientData("account").continue().continue().save();
// The error should inform about duplicated name/id // The error should inform about duplicated name/id
commonPage commonPage
@ -335,6 +336,7 @@ describe("Clients test", () => {
.clickOidcCibaGrant() .clickOidcCibaGrant()
.clickServiceAccountRoles() .clickServiceAccountRoles()
.clickStandardFlow() .clickStandardFlow()
.continue()
.save(); .save();
commonPage commonPage
@ -448,7 +450,11 @@ describe("Clients test", () => {
commonPage.sidebar().goToClients(); commonPage.sidebar().goToClients();
commonPage.tableToolbarUtils().createClient(); commonPage.tableToolbarUtils().createClient();
createClientPage.fillClientData(identicalClientId).continue().save(); createClientPage
.fillClientData(identicalClientId)
.continue()
.continue()
.save();
commonPage.masthead().closeAllAlertMessages(); commonPage.masthead().closeAllAlertMessages();
commonPage.sidebar().goToClients(); commonPage.sidebar().goToClients();
@ -493,6 +499,7 @@ describe("Clients test", () => {
.selectClientType("openid-connect") .selectClientType("openid-connect")
.fillClientData(client) .fillClientData(client)
.continue() .continue()
.continue()
.save(); .save();
commonPage commonPage
.masthead() .masthead()
@ -705,7 +712,7 @@ describe("Clients test", () => {
commonPage.sidebar().waitForPageLoad(); commonPage.sidebar().waitForPageLoad();
createClientPage.save(); createClientPage.continue().save();
commonPage commonPage
.masthead() .masthead()
.checkNotificationMessage("Client created successfully"); .checkNotificationMessage("Client created successfully");

View file

@ -130,6 +130,7 @@ describe("User Fed LDAP mapper tests", () => {
.selectClientType("openid-connect") .selectClientType("openid-connect")
.fillClientData(clientName) .fillClientData(clientName)
.continue() .continue()
.continue()
.save(); .save();
masthead.checkNotificationMessage(clientCreatedSuccess); masthead.checkNotificationMessage(clientCreatedSuccess);

View file

@ -5,14 +5,11 @@ import { useTranslation } from "react-i18next";
import { FormAccess } from "../../components/form-access/FormAccess"; import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { MultiLineInput } from "../../components/multi-line-input/hook-form-v7/MultiLineInput";
import { useAccess } from "../../context/access/Access"; import { useAccess } from "../../context/access/Access";
import { useRealm } from "../../context/realm-context/RealmContext";
import environment from "../../environment";
import { convertAttributeNameToForm } from "../../util";
import { SaveReset } from "../advanced/SaveReset"; import { SaveReset } from "../advanced/SaveReset";
import { FormFields } from "../ClientDetails"; import { FormFields } from "../ClientDetails";
import type { ClientSettingsProps } from "../ClientSettings"; import type { ClientSettingsProps } from "../ClientSettings";
import { LoginSettings } from "./LoginSettings";
export const AccessSettings = ({ export const AccessSettings = ({
client, client,
@ -21,15 +18,11 @@ export const AccessSettings = ({
}: ClientSettingsProps) => { }: ClientSettingsProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const { register, watch } = useFormContext<FormFields>(); const { register, watch } = useFormContext<FormFields>();
const { realm } = useRealm();
const { hasAccess } = useAccess(); const { hasAccess } = useAccess();
const isManager = hasAccess("manage-clients") || client.access?.configure; const isManager = hasAccess("manage-clients") || client.access?.configure;
const protocol = watch("protocol"); const protocol = watch("protocol");
const idpInitiatedSsoUrlName: string = watch(
"attributes.saml_idp_initiated_sso_url_name"
);
return ( return (
<FormAccess <FormAccess
@ -37,154 +30,7 @@ export const AccessSettings = ({
fineGrainedAccess={client.access?.configure} fineGrainedAccess={client.access?.configure}
role="manage-clients" role="manage-clients"
> >
{!client.bearerOnly && ( {!client.bearerOnly && <LoginSettings protocol={protocol} />}
<>
<FormGroup
label={t("rootUrl")}
fieldId="kc-root-url"
labelIcon={
<HelpItem
helpText="clients-help:rootURL"
fieldLabelId="clients:rootUrl"
/>
}
>
<KeycloakTextInput
id="kc-root-url"
type="url"
{...register("rootUrl")}
/>
</FormGroup>
<FormGroup
label={t("homeURL")}
fieldId="kc-home-url"
labelIcon={
<HelpItem
helpText="clients-help:homeURL"
fieldLabelId="clients:homeURL"
/>
}
>
<KeycloakTextInput
id="kc-home-url"
type="url"
{...register("baseUrl")}
/>
</FormGroup>
<FormGroup
label={t("validRedirectUri")}
fieldId="kc-redirect"
labelIcon={
<HelpItem
helpText="clients-help:validRedirectURIs"
fieldLabelId="clients:validRedirectUri"
/>
}
>
<MultiLineInput
name="redirectUris"
aria-label={t("validRedirectUri")}
addButtonLabel="clients:addRedirectUri"
/>
</FormGroup>
<FormGroup
label={t("validPostLogoutRedirectUri")}
fieldId="kc-postLogoutRedirect"
labelIcon={
<HelpItem
helpText="clients-help:validPostLogoutRedirectURIs"
fieldLabelId="clients:validPostLogoutRedirectUri"
/>
}
>
<MultiLineInput
name={convertAttributeNameToForm(
"attributes.post.logout.redirect.uris"
)}
aria-label={t("validPostLogoutRedirectUri")}
addButtonLabel="clients:addPostLogoutRedirectUri"
stringify
/>
</FormGroup>
{protocol === "saml" && (
<>
<FormGroup
label={t("idpInitiatedSsoUrlName")}
fieldId="idpInitiatedSsoUrlName"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoUrlName"
fieldLabelId="clients:idpInitiatedSsoUrlName"
/>
}
helperText={
idpInitiatedSsoUrlName !== "" &&
t("idpInitiatedSsoUrlNameHelp", {
url: `${environment.authServerUrl}/realms/${realm}/protocol/saml/clients/${idpInitiatedSsoUrlName}`,
})
}
>
<KeycloakTextInput
id="idpInitiatedSsoUrlName"
data-testid="idpInitiatedSsoUrlName"
{...register("attributes.saml_idp_initiated_sso_url_name")}
/>
</FormGroup>
<FormGroup
label={t("idpInitiatedSsoRelayState")}
fieldId="idpInitiatedSsoRelayState"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoRelayState"
fieldLabelId="clients:idpInitiatedSsoRelayState"
/>
}
>
<KeycloakTextInput
id="idpInitiatedSsoRelayState"
data-testid="idpInitiatedSsoRelayState"
{...register("attributes.saml_idp_initiated_sso_relay_state")}
/>
</FormGroup>
<FormGroup
label={t("masterSamlProcessingUrl")}
fieldId="masterSamlProcessingUrl"
labelIcon={
<HelpItem
helpText="clients-help:masterSamlProcessingUrl"
fieldLabelId="clients:masterSamlProcessingUrl"
/>
}
>
<KeycloakTextInput
id="masterSamlProcessingUrl"
type="url"
data-testid="masterSamlProcessingUrl"
{...register("adminUrl")}
/>
</FormGroup>
</>
)}
{protocol !== "saml" && (
<FormGroup
label={t("webOrigins")}
fieldId="kc-web-origins"
labelIcon={
<HelpItem
helpText="clients-help:webOrigins"
fieldLabelId="clients:webOrigins"
/>
}
>
<MultiLineInput
name="webOrigins"
aria-label={t("webOrigins")}
addButtonLabel="clients:addWebOrigins"
/>
</FormGroup>
)}
</>
)}
{protocol !== "saml" && ( {protocol !== "saml" && (
<FormGroup <FormGroup
label={t("adminURL")} label={t("adminURL")}

View file

@ -0,0 +1,178 @@
import { FormGroup } from "@patternfly/react-core";
import { useFormContext } from "react-hook-form-v7";
import { useTranslation } from "react-i18next";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import { MultiLineInput } from "../../components/multi-line-input/hook-form-v7/MultiLineInput";
import { useRealm } from "../../context/realm-context/RealmContext";
import environment from "../../environment";
import { convertAttributeNameToForm } from "../../util";
import { FormFields } from "../ClientDetails";
type LoginSettingsProps = {
protocol?: string;
};
export const LoginSettings = ({
protocol = "openid-connect",
}: LoginSettingsProps) => {
const { t } = useTranslation("clients");
const { register, watch } = useFormContext<FormFields>();
const { realm } = useRealm();
const idpInitiatedSsoUrlName: string = watch(
"attributes.saml_idp_initiated_sso_url_name"
);
return (
<>
<FormGroup
label={t("rootUrl")}
fieldId="kc-root-url"
labelIcon={
<HelpItem
helpText="clients-help:rootURL"
fieldLabelId="clients:rootUrl"
/>
}
>
<KeycloakTextInput
id="kc-root-url"
type="url"
{...register("rootUrl")}
/>
</FormGroup>
<FormGroup
label={t("homeURL")}
fieldId="kc-home-url"
labelIcon={
<HelpItem
helpText="clients-help:homeURL"
fieldLabelId="clients:homeURL"
/>
}
>
<KeycloakTextInput
id="kc-home-url"
type="url"
{...register("baseUrl")}
/>
</FormGroup>
<FormGroup
label={t("validRedirectUri")}
fieldId="kc-redirect"
labelIcon={
<HelpItem
helpText="clients-help:validRedirectURIs"
fieldLabelId="clients:validRedirectUri"
/>
}
>
<MultiLineInput
id="kc-redirect"
name="redirectUris"
aria-label={t("validRedirectUri")}
addButtonLabel="clients:addRedirectUri"
/>
</FormGroup>
<FormGroup
label={t("validPostLogoutRedirectUri")}
fieldId="kc-postLogoutRedirect"
labelIcon={
<HelpItem
helpText="clients-help:validPostLogoutRedirectURIs"
fieldLabelId="clients:validPostLogoutRedirectUri"
/>
}
>
<MultiLineInput
id="kc-postLogoutRedirect"
name={convertAttributeNameToForm(
"attributes.post.logout.redirect.uris"
)}
aria-label={t("validPostLogoutRedirectUri")}
addButtonLabel="clients:addPostLogoutRedirectUri"
stringify
/>
</FormGroup>
{protocol === "saml" && (
<>
<FormGroup
label={t("idpInitiatedSsoUrlName")}
fieldId="idpInitiatedSsoUrlName"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoUrlName"
fieldLabelId="clients:idpInitiatedSsoUrlName"
/>
}
helperText={
idpInitiatedSsoUrlName !== "" &&
t("idpInitiatedSsoUrlNameHelp", {
url: `${environment.authServerUrl}/realms/${realm}/protocol/saml/clients/${idpInitiatedSsoUrlName}`,
})
}
>
<KeycloakTextInput
id="idpInitiatedSsoUrlName"
data-testid="idpInitiatedSsoUrlName"
{...register("attributes.saml_idp_initiated_sso_url_name")}
/>
</FormGroup>
<FormGroup
label={t("idpInitiatedSsoRelayState")}
fieldId="idpInitiatedSsoRelayState"
labelIcon={
<HelpItem
helpText="clients-help:idpInitiatedSsoRelayState"
fieldLabelId="clients:idpInitiatedSsoRelayState"
/>
}
>
<KeycloakTextInput
id="idpInitiatedSsoRelayState"
data-testid="idpInitiatedSsoRelayState"
{...register("attributes.saml_idp_initiated_sso_relay_state")}
/>
</FormGroup>
<FormGroup
label={t("masterSamlProcessingUrl")}
fieldId="masterSamlProcessingUrl"
labelIcon={
<HelpItem
helpText="clients-help:masterSamlProcessingUrl"
fieldLabelId="clients:masterSamlProcessingUrl"
/>
}
>
<KeycloakTextInput
id="masterSamlProcessingUrl"
type="url"
data-testid="masterSamlProcessingUrl"
{...register("adminUrl")}
/>
</FormGroup>
</>
)}
{protocol !== "saml" && (
<FormGroup
label={t("webOrigins")}
fieldId="kc-web-origins"
labelIcon={
<HelpItem
helpText="clients-help:webOrigins"
fieldLabelId="clients:webOrigins"
/>
}
>
<MultiLineInput
id="kc-web-origins"
name="webOrigins"
aria-label={t("webOrigins")}
addButtonLabel="clients:addWebOrigins"
/>
</FormGroup>
)}
</>
);
};

View file

@ -1,4 +1,3 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -11,7 +10,9 @@ import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form-v7"; import { FormProvider, useForm } from "react-hook-form-v7";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom-v5-compat"; import { useNavigate } from "react-router-dom-v5-compat";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { FormAccess } from "../../components/form-access/FormAccess";
import { ViewHeader } from "../../components/view-header/ViewHeader"; import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useAdminClient } from "../../context/auth/AdminClient"; import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
@ -21,6 +22,7 @@ import { toClient } from "../routes/Client";
import { toClients } from "../routes/Clients"; import { toClients } from "../routes/Clients";
import { CapabilityConfig } from "./CapabilityConfig"; import { CapabilityConfig } from "./CapabilityConfig";
import { GeneralSettings } from "./GeneralSettings"; import { GeneralSettings } from "./GeneralSettings";
import { LoginSettings } from "./LoginSettings";
export default function NewClientForm() { export default function NewClientForm() {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
@ -28,8 +30,11 @@ export default function NewClientForm() {
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const navigate = useNavigate(); const navigate = useNavigate();
const [showCapabilityConfig, setShowCapabilityConfig] = useState(false); const [step, setStep] = useState(0);
const [client, setClient] = useState<ClientRepresentation>({
const { addAlert, addError } = useAlerts();
const form = useForm<FormFields>({
defaultValues: {
protocol: "openid-connect", protocol: "openid-connect",
clientId: "", clientId: "",
name: "", name: "",
@ -41,12 +46,16 @@ export default function NewClientForm() {
directAccessGrantsEnabled: true, directAccessGrantsEnabled: true,
standardFlowEnabled: true, standardFlowEnabled: true,
frontchannelLogout: true, frontchannelLogout: true,
attributes: {
saml_idp_initiated_sso_url_name: "",
},
},
}); });
const { addAlert, addError } = useAlerts(); const { getValues, watch, trigger } = form;
const methods = useForm<FormFields>({ defaultValues: client }); const protocol = watch("protocol");
const protocol = methods.watch("protocol");
const save = async () => { const save = async () => {
const client = convertFormValuesToObject(getValues());
try { try {
const newClient = await adminClient.clients.create({ const newClient = await adminClient.clients.create({
...client, ...client,
@ -60,35 +69,29 @@ export default function NewClientForm() {
}; };
const forward = async (onNext?: () => void) => { const forward = async (onNext?: () => void) => {
if (await methods.trigger()) { if (!(await trigger())) {
setClient({ return;
...client, }
...convertFormValuesToObject(methods.getValues()),
});
if (!isFinalStep()) { if (!isFinalStep()) {
setShowCapabilityConfig(true); setStep(step + 1);
} }
onNext?.(); onNext?.();
}
}; };
const isFinalStep = () => const isFinalStep = () =>
showCapabilityConfig || protocol !== "openid-connect"; protocol === "openid-connect" ? step === 2 : step === 1;
const back = () => { const back = () => {
setClient({ ...client, ...convertFormValuesToObject(methods.getValues()) }); setStep(step - 1);
methods.reset({
...client,
...convertFormValuesToObject(methods.getValues()),
});
setShowCapabilityConfig(false);
}; };
const onGoToStep = (newStep: { id?: string | number }) => { const onGoToStep = (newStep: { id?: string | number }) => {
if (newStep.id === "generalSettings") { if (newStep.id === "generalSettings") {
back(); setStep(0);
} else if (newStep.id === "capabilityConfig") {
setStep(1);
} else { } else {
forward(); setStep(2);
} }
}; };
@ -135,7 +138,7 @@ export default function NewClientForm() {
subKey="clients:clientsExplain" subKey="clients:clientsExplain"
/> />
<PageSection variant="light"> <PageSection variant="light">
<FormProvider {...methods}> <FormProvider {...form}>
<Wizard <Wizard
onClose={() => navigate(toClients({ realm }))} onClose={() => navigate(toClients({ realm }))}
navAriaLabel={`${title} steps`} navAriaLabel={`${title} steps`}
@ -146,17 +149,26 @@ export default function NewClientForm() {
name: t("generalSettings"), name: t("generalSettings"),
component: <GeneralSettings />, component: <GeneralSettings />,
}, },
...(showCapabilityConfig ...(protocol !== "saml"
? [ ? [
{ {
id: "capabilityConfig", id: "capabilityConfig",
name: t("capabilityConfig"), name: t("capabilityConfig"),
component: ( component: <CapabilityConfig protocol={protocol} />,
<CapabilityConfig protocol={client.protocol} /> canJumpTo: step >= 1,
),
}, },
] ]
: []), : []),
{
id: "loginSettings",
name: t("loginSettings"),
component: (
<FormAccess isHorizontal role="manage-clients">
<LoginSettings protocol={protocol} />
</FormAccess>
),
canJumpTo: step >= 1,
},
]} ]}
footer={<Footer />} footer={<Footer />}
onSave={save} onSave={save}