Add extended fields for identity providers (#3167)

This commit is contained in:
Jon Koops 2022-08-23 15:53:34 +02:00 committed by GitHub
parent b9e2ba2a2f
commit a9e767da57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 317 additions and 6 deletions

View file

@ -90,5 +90,24 @@
"attributeValue": "Value the attribute must have. If the attribute is a list, then the value must be contained in the list.", "attributeValue": "Value the attribute must have. If the attribute is a list, then the value must be contained in the list.",
"attributes": "Name and (regex) value of the attributes to search for in token. The configured name of an attribute is searched in SAML attribute name and attribute friendly name fields. Every given attribute description must be met to set the role. If the attribute is an array, then the value must be contained in the array. If an attribute can be found several times, then one match is sufficient.", "attributes": "Name and (regex) value of the attributes to search for in token. The configured name of an attribute is searched in SAML attribute name and attribute friendly name fields. Every given attribute description must be met to set the role. If the attribute is an array, then the value must be contained in the array. If an attribute can be found several times, then one match is sufficient.",
"regexAttributeValues": "If enabled attribute values are interpreted as regular expressions.", "regexAttributeValues": "If enabled attribute values are interpreted as regular expressions.",
"role": "Role to grant to user if all attributes are present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference a client role the syntax is clientname.clientrole, i.e. myclient.myrole" "role": "Role to grant to user if all attributes are present. Click 'Select Role' button to browse roles, or just type it in the textbox. To reference a client role the syntax is clientname.clientrole, i.e. myclient.myrole",
"baseUrl": "Override the default Base URL for this identity provider.",
"apiUrl": "Override the default API URL for this identity provider.",
"facebook": {
"fetchedFields": "Provide additional fields which would be fetched using the profile request. This will be appended to the default set of 'id,name,email,first_name,last_name'."
},
"google": {
"hostedDomain": "Set 'hd' query parameter when logging in with Google. Google will list accounts only for this domain. Keycloak validates that the returned identity token has a claim for this domain. When '*' is entered, any hosted account can be used.",
"userIp": "Set 'userIp' query parameter when invoking on Google's User Info service. This will use the user's ip address. Useful if Google is throttling access to the User Info service.",
"offlineAccess": "Set 'access_type' query parameter to 'offline' when redirecting to google authorization endpoint, to get a refresh token back. Useful if planning to use Token Exchange to retrieve Google token to access Google APIs when the user is not at the browser."
},
"openshift": {
"baseUrl": "Base Url to OpenShift Online API"
},
"paypal": {
"sandbox": "Target PayPal's sandbox environment"
},
"stackoverflow": {
"key": "The Key obtained from Stack Overflow client registration."
}
} }

View file

@ -173,5 +173,21 @@
"local": "LOCAL", "local": "LOCAL",
"brokerId": "BROKER_ID", "brokerId": "BROKER_ID",
"brokerUsername": "BROKER_USERNAME" "brokerUsername": "BROKER_USERNAME"
},
"baseUrl": "Base URL",
"apiUrl": "API URL",
"facebook": {
"fetchedFields": "Additional user's profile fields"
},
"google": {
"hostedDomain": "Hosted Domain",
"userIp": "Use userIp Param",
"offlineAccess": "Request refresh token"
},
"paypal": {
"sandbox": "Target Sandbox"
},
"stackoverflow": {
"key": "Key"
} }
} }

View file

@ -10,6 +10,7 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation"; import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { ExtendedFieldsForm } from "../component/ExtendedFieldsForm";
import { ViewHeader } from "../../components/view-header/ViewHeader"; import { ViewHeader } from "../../components/view-header/ViewHeader";
import { toUpperCase } from "../../util"; import { toUpperCase } from "../../util";
import { FormAccess } from "../../components/form-access/FormAccess"; import { FormAccess } from "../../components/form-access/FormAccess";
@ -46,8 +47,8 @@ export default function AddIdentityProvider() {
navigate( navigate(
toIdentityProvider({ toIdentityProvider({
realm, realm,
providerId: providerId!, providerId,
alias: providerId!, alias: providerId,
tab: "settings", tab: "settings",
}) })
); );
@ -60,7 +61,7 @@ export default function AddIdentityProvider() {
<> <>
<ViewHeader <ViewHeader
titleKey={t("addIdentityProvider", { titleKey={t("addIdentityProvider", {
provider: toUpperCase(providerId!), provider: toUpperCase(providerId),
})} })}
/> />
<PageSection variant="light"> <PageSection variant="light">
@ -70,7 +71,8 @@ export default function AddIdentityProvider() {
onSubmit={handleSubmit(save)} onSubmit={handleSubmit(save)}
> >
<FormProvider {...form}> <FormProvider {...form}>
<GeneralSettings id={providerId!} /> <GeneralSettings id={providerId} />
<ExtendedFieldsForm providerId={providerId} />
</FormProvider> </FormProvider>
<ActionGroup> <ActionGroup>
<Button <Button

View file

@ -55,6 +55,7 @@ import {
toIdentityProvider, toIdentityProvider,
} from "../routes/IdentityProvider"; } from "../routes/IdentityProvider";
import { PermissionsTab } from "../../components/permission-tab/PermissionTab"; import { PermissionsTab } from "../../components/permission-tab/PermissionTab";
import { ExtendedFieldsForm } from "../component/ExtendedFieldsForm";
type HeaderProps = { type HeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -276,7 +277,12 @@ export default function DetailSettings() {
isHorizontal isHorizontal
onSubmit={handleSubmit(save)} onSubmit={handleSubmit(save)}
> >
{!isOIDC && !isSAML && <GeneralSettings create={false} id={alias} />} {!isOIDC && !isSAML && (
<>
<GeneralSettings create={false} id={alias} />
<ExtendedFieldsForm providerId={alias} />
</>
)}
{isOIDC && <OIDCGeneralSettings id={alias} />} {isOIDC && <OIDCGeneralSettings id={alias} />}
{isSAML && <SamlGeneralSettings id={alias} />} {isSAML && <SamlGeneralSettings id={alias} />}
</FormAccess> </FormAccess>

View file

@ -0,0 +1,268 @@
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
type ExtendedFieldsFormProps = {
providerId: string;
};
export const ExtendedFieldsForm = ({ providerId }: ExtendedFieldsFormProps) => {
switch (providerId) {
case "facebook":
return <FacebookFields />;
case "github":
return <GithubFields />;
case "google":
return <GoogleFields />;
case "openshift-v3":
case "openshift-v4":
return <OpenshiftFields />;
case "paypal":
return <PaypalFields />;
case "stackoverflow":
return <StackoverflowFields />;
default:
return null;
}
};
const FacebookFields = () => {
const { t } = useTranslation("identity-providers");
const { register } = useFormContext();
return (
<FormGroup
label={t("facebook.fetchedFields")}
labelIcon={
<HelpItem
helpText="identity-providers-help:facebook:fetchedFields"
fieldLabelId="identity-providers:facebook:fetchedFields"
/>
}
fieldId="facebookFetchedFields"
>
<KeycloakTextInput
id="facebookFetchedFields"
name="config.fetchedFields"
ref={register()}
/>
</FormGroup>
);
};
const GithubFields = () => {
const { t } = useTranslation("identity-providers");
const { register } = useFormContext();
return (
<>
<FormGroup
label={t("baseUrl")}
labelIcon={
<HelpItem
helpText="identity-providers-help:baseUrl"
fieldLabelId="identity-providers:baseUrl"
/>
}
fieldId="baseUrl"
>
<KeycloakTextInput
id="baseUrl"
name="config.baseUrl"
ref={register()}
/>
</FormGroup>
<FormGroup
label={t("apiUrl")}
labelIcon={
<HelpItem
helpText="identity-providers-help:apiUrl"
fieldLabelId="identity-providers:apiUrl"
/>
}
fieldId="apiUrl"
>
<KeycloakTextInput id="apiUrl" name="config.apiUrl" ref={register()} />
</FormGroup>
</>
);
};
const GoogleFields = () => {
const { t } = useTranslation("identity-providers");
const { register, control } = useFormContext();
return (
<>
<FormGroup
label={t("google.hostedDomain")}
labelIcon={
<HelpItem
helpText="identity-providers-help:google:hostedDomain"
fieldLabelId="identity-providers:google:hostedDomain"
/>
}
fieldId="googleHostedDomain"
>
<KeycloakTextInput
id="googleHostedDomain"
name="config.hostedDomain"
ref={register()}
/>
</FormGroup>
<FormGroup
label={t("google.userIp")}
labelIcon={
<HelpItem
helpText="identity-providers-help:google:userIp"
fieldLabelId="identity-providers:google:userIp"
/>
}
fieldId="googleUserIp"
>
<Controller
name="config.userIp"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
id="googleUserIp"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("google.offlineAccess")}
labelIcon={
<HelpItem
helpText="identity-providers-help:google:offlineAccess"
fieldLabelId="identity-providers:google:offlineAccess"
/>
}
fieldId="googleOfflineAccess"
>
<Controller
name="config.offlineAccess"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
id="googleOfflineAccess"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
</>
);
};
const OpenshiftFields = () => {
const { t } = useTranslation("identity-providers");
const {
register,
formState: { errors },
} = useFormContext();
return (
<FormGroup
label={t("baseUrl")}
labelIcon={
<HelpItem
helpText="identity-providers-help:openshift:baseUrl"
fieldLabelId="identity-providers:baseUrl"
/>
}
fieldId="baseUrl"
isRequired
validated={
errors.config?.baseUrl
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
id="baseUrl"
name="config.baseUrl"
ref={register({ required: true })}
isRequired
/>
</FormGroup>
);
};
const PaypalFields = () => {
const { t } = useTranslation("identity-providers");
const { control } = useFormContext();
return (
<FormGroup
label={t("paypal.sandbox")}
labelIcon={
<HelpItem
helpText="identity-providers-help:paypal:sandbox"
fieldLabelId="identity-providers:paypal:sandbox"
/>
}
fieldId="paypalSandbox"
>
<Controller
name="config.sandbox"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Switch
id="paypalSandbox"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange(value.toString())}
/>
)}
/>
</FormGroup>
);
};
const StackoverflowFields = () => {
const { t } = useTranslation("identity-providers");
const {
register,
formState: { errors },
} = useFormContext();
return (
<FormGroup
label={t("stackoverflow.key")}
labelIcon={
<HelpItem
helpText="identity-providers-help:stackoverflow:key"
fieldLabelId="identity-providers:stackoverflow:key"
/>
}
fieldId="stackoverflowKey"
isRequired
validated={
errors.config?.key ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
id="stackoverflowKey"
name="config.key"
ref={register({ required: true })}
isRequired
/>
</FormGroup>
);
};