changed to use ConfiguredProvider instead (#21097)

fixes: #15344
This commit is contained in:
Erik Jan de Wit 2023-06-27 14:00:32 +02:00 committed by GitHub
parent a51fe1d961
commit 3a3907ab15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 199 additions and 384 deletions

View file

@ -95,27 +95,5 @@
"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.",
"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",
"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. Comma ',' separated list of domains is supported.",
"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."
},
"linkedin": {
"profileProjection": "Projection parameter for profile request. Leave empty for default projection."
}
"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"
}

View file

@ -178,24 +178,5 @@
"local": "LOCAL",
"brokerId": "BROKER_ID",
"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"
},
"linkedin": {
"profileProjection": "Profile Projection"
}
}

View file

@ -5,17 +5,19 @@ import {
Button,
PageSection,
} from "@patternfly/react-core";
import { useMemo } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { adminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { DynamicComponents } from "../../components/dynamic/DynamicComponents";
import { FormAccess } from "../../components/form/FormAccess";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { toUpperCase } from "../../util";
import { useParams } from "../../utils/useParams";
import { ExtendedFieldsForm } from "../component/ExtendedFieldsForm";
import { toIdentityProvider } from "../routes/IdentityProvider";
import type { IdentityProviderCreateParams } from "../routes/IdentityProviderCreate";
import { toIdentityProviders } from "../routes/IdentityProviders";
@ -25,6 +27,14 @@ export default function AddIdentityProvider() {
const { t } = useTranslation("identity-providers");
const { providerId } = useParams<IdentityProviderCreateParams>();
const form = useForm<IdentityProviderRepresentation>();
const serverInfo = useServerInfo();
const providerInfo = useMemo(
() =>
serverInfo.componentTypes?.[
"org.keycloak.broker.social.SocialIdentityProvider"
]?.find((p) => p.id === providerId),
[serverInfo, providerId]
);
const {
handleSubmit,
formState: { isDirty },
@ -70,7 +80,9 @@ export default function AddIdentityProvider() {
>
<FormProvider {...form}>
<GeneralSettings id={providerId} />
<ExtendedFieldsForm providerId={providerId} />
{providerInfo && (
<DynamicComponents properties={providerInfo.properties} />
)}
</FormProvider>
<ActionGroup>
<Button

View file

@ -12,7 +12,7 @@ import {
TabTitleText,
ToolbarItem,
} from "@patternfly/react-core";
import { useState } from "react";
import { useMemo, useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
@ -20,6 +20,7 @@ import { Link, useNavigate } from "react-router-dom";
import { adminClient } from "../../admin-client";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { DynamicComponents } from "../../components/dynamic/DynamicComponents";
import { FixedButtonsGroup } from "../../components/form/FixedButtonGroup";
import { FormAccess } from "../../components/form/FormAccess";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
@ -36,11 +37,11 @@ import {
} from "../../components/table-toolbar/KeycloakDataTable";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { toUpperCase } from "../../util";
import { useFetch } from "../../utils/useFetch";
import useIsFeatureEnabled, { Feature } from "../../utils/useIsFeatureEnabled";
import { useParams } from "../../utils/useParams";
import { ExtendedFieldsForm } from "../component/ExtendedFieldsForm";
import { toIdentityProviderAddMapper } from "../routes/AddMapper";
import { toIdentityProviderEditMapper } from "../routes/EditMapper";
import {
@ -162,6 +163,14 @@ export default function DetailSettings() {
const [provider, setProvider] = useState<IdentityProviderRepresentation>();
const [selectedMapper, setSelectedMapper] =
useState<IdPWithMapperAttributes>();
const serverInfo = useServerInfo();
const providerInfo = useMemo(
() =>
serverInfo.componentTypes?.[
"org.keycloak.broker.social.SocialIdentityProvider"
]?.find((p) => p.id === providerId),
[serverInfo, providerId]
);
const { addAlert, addError } = useAlerts();
const navigate = useNavigate();
@ -321,7 +330,9 @@ export default function DetailSettings() {
{!isOIDC && !isSAML && (
<>
<GeneralSettings create={false} id={alias} />
<ExtendedFieldsForm providerId={alias} />
{providerInfo && (
<DynamicComponents properties={providerInfo.properties} />
)}
</>
)}
{isOIDC && <OIDCGeneralSettings id={alias} />}

View file

@ -1,299 +0,0 @@
import IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { FormGroup, Switch, ValidatedOptions } from "@patternfly/react-core";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
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 />;
case "linkedin":
return <LinkedInFields />;
default:
return null;
}
};
const FacebookFields = () => {
const { t } = useTranslation("identity-providers");
const { register } = useFormContext();
return (
<FormGroup
label={t("facebook.fetchedFields")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:facebook:fetchedFields")}
fieldLabelId="identity-providers:facebook:fetchedFields"
/>
}
fieldId="facebookFetchedFields"
>
<KeycloakTextInput
id="facebookFetchedFields"
{...register("config.fetchedFields")}
/>
</FormGroup>
);
};
const GithubFields = () => {
const { t } = useTranslation("identity-providers");
const { register } = useFormContext();
return (
<>
<FormGroup
label={t("baseUrl")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:baseUrl")}
fieldLabelId="identity-providers:baseUrl"
/>
}
fieldId="baseUrl"
>
<KeycloakTextInput
id="baseUrl"
type="url"
{...register("config.baseUrl")}
/>
</FormGroup>
<FormGroup
label={t("apiUrl")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:apiUrl")}
fieldLabelId="identity-providers:apiUrl"
/>
}
fieldId="apiUrl"
>
<KeycloakTextInput
id="apiUrl"
type="url"
{...register("config.apiUrl")}
/>
</FormGroup>
</>
);
};
const GoogleFields = () => {
const { t } = useTranslation("identity-providers");
const { register, control } = useFormContext();
return (
<>
<FormGroup
label={t("google.hostedDomain")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:google:hostedDomain")}
fieldLabelId="identity-providers:google:hostedDomain"
/>
}
fieldId="googleHostedDomain"
>
<KeycloakTextInput
id="googleHostedDomain"
{...register("config.hostedDomain")}
/>
</FormGroup>
<FormGroup
label={t("google.userIp")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:google:userIp")}
fieldLabelId="identity-providers:google:userIp"
/>
}
fieldId="googleUserIp"
>
<Controller
name="config.userIp"
defaultValue="false"
control={control}
render={({ field }) => (
<Switch
id="googleUserIp"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("google.userIp")}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("google.offlineAccess")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:google:offlineAccess")}
fieldLabelId="identity-providers:google:offlineAccess"
/>
}
fieldId="googleOfflineAccess"
>
<Controller
name="config.offlineAccess"
defaultValue="false"
control={control}
render={({ field }) => (
<Switch
id="googleOfflineAccess"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("google.offlineAccess")}
/>
)}
/>
</FormGroup>
</>
);
};
const OpenshiftFields = () => {
const { t } = useTranslation("identity-providers");
const {
register,
formState: { errors },
} = useFormContext<IdentityProviderRepresentation>();
return (
<FormGroup
label={t("baseUrl")}
labelIcon={
<HelpItem
helpText={t("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"
type="url"
isRequired
{...register("config.baseUrl", { required: true })}
/>
</FormGroup>
);
};
const PaypalFields = () => {
const { t } = useTranslation("identity-providers");
const { control } = useFormContext();
return (
<FormGroup
label={t("paypal.sandbox")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:paypal:sandbox")}
fieldLabelId="identity-providers:paypal:sandbox"
/>
}
fieldId="paypalSandbox"
>
<Controller
name="config.sandbox"
defaultValue="false"
control={control}
render={({ field }) => (
<Switch
id="paypalSandbox"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => field.onChange(value.toString())}
aria-label={t("paypal.sandbox")}
/>
)}
/>
</FormGroup>
);
};
const StackoverflowFields = () => {
const { t } = useTranslation("identity-providers");
const {
register,
formState: { errors },
} = useFormContext<IdentityProviderRepresentation>();
return (
<FormGroup
label={t("stackoverflow.key")}
labelIcon={
<HelpItem
helpText={t("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"
isRequired
{...register("config.key", { required: true })}
/>
</FormGroup>
);
};
const LinkedInFields = () => {
const { t } = useTranslation("identity-providers");
const { register } = useFormContext();
return (
<FormGroup
label={t("linkedin.profileProjection")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:linkedin.profileProjection")}
fieldLabelId="identity-providers:linkedin.profileProjection"
/>
}
fieldId="profileProjection"
>
<KeycloakTextInput
id="profileProjection"
{...register("config.profileProjection")}
/>
</FormGroup>
);
};

View file

@ -18,15 +18,19 @@ package org.keycloak.broker.provider;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ConfiguredProvider;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* @author Pedro Igor
*/
public interface IdentityProviderFactory<T extends IdentityProvider> extends ProviderFactory<T> {
public interface IdentityProviderFactory<T extends IdentityProvider> extends ProviderFactory<T>, ConfiguredProvider {
/**
* <p>A friendly name for this factory.</p>
@ -64,4 +68,10 @@ public interface IdentityProviderFactory<T extends IdentityProvider> extends Pro
* @return the provider specific instance
*/
IdentityProviderModel createConfig();
default List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
default String getHelpText() { return ""; }
}

View file

@ -21,6 +21,10 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* @author Pedro Igor
@ -48,4 +52,14 @@ public class FacebookIdentityProviderFactory extends AbstractIdentityProviderFac
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name("fetchedFields")
.label("Additional user's profile fields")
.helpText("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'.")
.type(ProviderConfigProperty.STRING_TYPE)
.add().build();
}
}

View file

@ -21,6 +21,10 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* @author Pedro Igor
@ -48,4 +52,13 @@ public class GitHubIdentityProviderFactory extends AbstractIdentityProviderFacto
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create().property()
.name("baseUrl").label("Base URL").helpText("Override the default Base URL for this identity provider.")
.type(ProviderConfigProperty.STRING_TYPE).add().property()
.name("apiUrl").label("API URL").helpText("Override the default API URL for this identity provider.")
.type(ProviderConfigProperty.STRING_TYPE).add().build();
}
}

View file

@ -20,6 +20,10 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* @author Pedro Igor
@ -47,4 +51,27 @@ public class GoogleIdentityProviderFactory extends AbstractIdentityProviderFacto
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name("hostedDomain")
.label("Hosted Domain")
.helpText("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. Comma ',' separated list of domains is supported.")
.type(ProviderConfigProperty.STRING_TYPE).add()
.property().name("userIp")
.label("Use userIp param")
.helpText("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.")
.type(ProviderConfigProperty.BOOLEAN_TYPE).add()
.property().name("offlineAccess")
.label("Request refresh token")
.helpText("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.")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.add().build();
}
}

View file

@ -21,6 +21,10 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* @author Vlastimil Elias (velias at redhat dot com)
@ -49,4 +53,13 @@ public class LinkedInIdentityProviderFactory extends AbstractIdentityProviderFac
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name("profileProjection")
.label("Profile projection")
.helpText("Projection parameter for profile request. Leave empty for default projection.")
.add().build();
}
}

View file

@ -4,6 +4,9 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
public class OpenshiftV3IdentityProviderFactory extends AbstractIdentityProviderFactory<OpenshiftV3IdentityProvider> implements SocialIdentityProviderFactory<OpenshiftV3IdentityProvider> {
@ -29,4 +32,8 @@ public class OpenshiftV3IdentityProviderFactory extends AbstractIdentityProvider
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return OpenshiftV4IdentityProviderConfig.getConfigProperties();
}
}

View file

@ -2,7 +2,10 @@ package org.keycloak.social.openshift;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -36,4 +39,13 @@ public class OpenshiftV4IdentityProviderConfig extends OAuth2IdentityProviderCon
public void setBaseUrl(String baseUrl) {
getConfig().put(BASE_URL, trimTrailingSlash(baseUrl));
}
public static List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name(BASE_URL)
.label("Base URL")
.helpText("Override the default Base URL for this identity provider.")
.type(ProviderConfigProperty.STRING_TYPE)
.add().build();
}
}

View file

@ -4,6 +4,9 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* OpenShift 4 Identity Provider factory class.
@ -34,4 +37,9 @@ public class OpenshiftV4IdentityProviderFactory extends AbstractIdentityProvider
public OpenshiftV4IdentityProviderConfig createConfig() {
return new OpenshiftV4IdentityProviderConfig();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return OpenshiftV4IdentityProviderConfig.getConfigProperties();
}
}

View file

@ -21,6 +21,10 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* @author Petter Lysne
@ -48,4 +52,14 @@ public class PayPalIdentityProviderFactory extends AbstractIdentityProviderFacto
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name("sandbox")
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.label("Target Sandbox")
.helpText("Target PayPal's sandbox environment")
.add().build();
}
}

View file

@ -20,6 +20,10 @@ import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.broker.social.SocialIdentityProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* @author Vlastimil Elias (velias at redhat dot com)
@ -49,4 +53,14 @@ public class StackoverflowIdentityProviderFactory extends
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return ProviderConfigurationBuilder.create()
.property().name("key")
.type(ProviderConfigProperty.STRING_TYPE)
.label("Key")
.helpText("The Key obtained from Stack Overflow client registration.")
.add().build();
}
}