From 651d99db25f088b7dadf34bef81fc478cae3efb4 Mon Sep 17 00:00:00 2001 From: Marek Posolda Date: Thu, 25 Jan 2024 17:01:02 +0100 Subject: [PATCH] Allow selecting attributes from user profile when managing token mappers (#26415) * Allow selecting attributes from user profile when managing token mappers closes #24250 Signed-off-by: mposolda Co-authored-by: Jon Koops --- .../HardcodedAttributeMapperFactory.java | 2 +- ...UserAttributeLDAPStorageMapperFactory.java | 2 +- .../tabs/mappers/MapperDetailsPage.ts | 2 +- .../identity_providers/AddMapperPage.ts | 6 +- .../admin-ui/manage/providers/ProviderPage.ts | 6 +- .../UserProfileAttributeListComponent.tsx | 61 +++++++++++++++++++ .../src/components/dynamic/components.ts | 6 +- .../provider/ProviderConfigProperty.java | 5 ++ .../AbstractJsonUserAttributeMapper.java | 2 +- .../oidc/mappers/UserAttributeMapper.java | 2 +- .../provider/HardcodedAttributeMapper.java | 2 +- .../saml/mappers/UserAttributeMapper.java | 2 +- .../saml/mappers/XPathAttributeMapper.java | 2 +- .../oidc/mappers/UserAttributeMapper.java | 2 +- .../mappers/UserAttributeStatementMapper.java | 1 + .../UserPropertyAttributeStatementMapper.java | 1 + 16 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 js/apps/admin-ui/src/components/dynamic/UserProfileAttributeListComponent.tsx diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedAttributeMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedAttributeMapperFactory.java index f9f3e5ec68f0..702931f3e278 100644 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedAttributeMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedAttributeMapperFactory.java @@ -39,7 +39,7 @@ public class HardcodedAttributeMapperFactory extends AbstractLDAPStorageMapperFa ProviderConfigProperty attrName = createConfigProperty(HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute Name", "Name of the model attribute, which will be added when importing user from ldap", - ProviderConfigProperty.STRING_TYPE, + ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE, null, true); diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java index e13ccd32ef3c..533d574484b9 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/UserAttributeLDAPStorageMapperFactory.java @@ -55,7 +55,7 @@ static List getConfigProps(ComponentModel p) { .property().name(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE) .label("User Model Attribute") .helpText("Name of the UserModel property or attribute you want to map the LDAP attribute into. For example 'firstName', 'lastName, 'email', 'street' etc.") - .type(ProviderConfigProperty.STRING_TYPE) + .type(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE) .required(true) .add() .property().name(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE).label("LDAP Attribute").helpText("Name of mapped attribute on LDAP object. For example 'cn', 'sn, 'mail', 'street' etc.") diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/client_scopes/client_scope_details/tabs/mappers/MapperDetailsPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/client_scopes/client_scope_details/tabs/mappers/MapperDetailsPage.ts index 32ff7567bb50..951f4d7cb7df 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/client_scopes/client_scope_details/tabs/mappers/MapperDetailsPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/client_scopes/client_scope_details/tabs/mappers/MapperDetailsPage.ts @@ -9,7 +9,7 @@ export enum ClaimJsonType { } export default class MapperDetailsPage extends CommonPage { - #userAttributeInput = '[id="user.attribute"]'; + #userAttributeInput = '[data-testid="config.user🍺attribute"]'; #tokenClaimNameInput = '[id="claim.name"]'; #claimJsonType = '[id="jsonType.label"]'; diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/AddMapperPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/AddMapperPage.ts index c9fe89607bde..d781912b66de 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/AddMapperPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/identity_providers/AddMapperPage.ts @@ -8,13 +8,13 @@ export default class AddMapperPage { #addMapperButton = "#add-mapper-button"; #mapperNameInput = "#kc-name"; - #attribute = "user.attribute"; + #attribute = "config.user🍺attribute"; #attributeName = "attribute.name"; #attributeFriendlyName = "attribute.friendly.name"; #claimInput = "claim"; #socialProfileJSONfieldPath = "jsonField"; - #userAttribute = "attribute"; - #userAttributeName = "userAttribute"; + #userAttribute = "config.attribute"; + #userAttributeName = "config.userAttribute"; #userAttributeValue = "attribute.value"; #userSessionAttribute = "attribute"; #userSessionAttributeValue = "attribute.value"; diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/providers/ProviderPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/providers/ProviderPage.ts index 14ab64b0528f..741fc472034e 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/providers/ProviderPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/providers/ProviderPage.ts @@ -63,9 +63,9 @@ export default class ProviderPage { #cachePolicyList = "#kc-cache-policy + ul"; // Mapper input values - #userModelAttInput = "user.model.attribute"; + #userModelAttInput = "config.user🍺model🍺attribute"; #ldapAttInput = "ldap.attribute"; - #userModelAttNameInput = "user.model.attribute"; + #userModelAttNameInput = "config.user🍺model🍺attribute"; #attValueInput = "attribute.value"; #ldapFullNameAttInput = "ldap.full.name.attribute"; #ldapAttNameInput = "ldap.attribute.name"; @@ -317,7 +317,7 @@ export default class ProviderPage { } createNewMapper(mapperType: string) { - const userModelAttValue = "firstName"; + const userModelAttValue = "middleName"; const ldapAttValue = "cn"; const ldapDnValue = "ou=groups"; diff --git a/js/apps/admin-ui/src/components/dynamic/UserProfileAttributeListComponent.tsx b/js/apps/admin-ui/src/components/dynamic/UserProfileAttributeListComponent.tsx new file mode 100644 index 000000000000..3fb77f01d9b9 --- /dev/null +++ b/js/apps/admin-ui/src/components/dynamic/UserProfileAttributeListComponent.tsx @@ -0,0 +1,61 @@ +import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata"; +import { FormGroup } from "@patternfly/react-core"; +import { useState } from "react"; +import { useFormContext } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { HelpItem } from "ui-shared"; + +import { adminClient } from "../../admin-client"; +import { useFetch } from "../../utils/useFetch"; +import { KeySelect } from "../key-value-form/KeySelect"; +import { convertToName } from "./DynamicComponents"; +import type { ComponentProps } from "./components"; + +export const UserProfileAttributeListComponent = ({ + name, + label, + helpText, + required = false, +}: ComponentProps) => { + const { t } = useTranslation(); + const { + formState: { errors }, + } = useFormContext(); + + const [config, setConfig] = useState(); + const convertedName = convertToName(name!); + + useFetch( + () => adminClient.users.getProfile(), + (cfg) => setConfig(cfg), + [], + ); + + const convert = (config?: UserProfileConfig) => { + if (!config?.attributes) return []; + + return config.attributes.map((option) => ({ + key: option.name!, + label: option.name!, + })); + }; + + if (!config) return null; + + return ( + } + fieldId={convertedName!} + validated={errors[convertedName!] ? "error" : "default"} + helperTextInvalid={t("required")} + > + + + ); +}; diff --git a/js/apps/admin-ui/src/components/dynamic/components.ts b/js/apps/admin-ui/src/components/dynamic/components.ts index 1f93cd64c5ed..f860088f4ce4 100644 --- a/js/apps/admin-ui/src/components/dynamic/components.ts +++ b/js/apps/admin-ui/src/components/dynamic/components.ts @@ -1,4 +1,5 @@ import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation"; +import { FunctionComponent } from "react"; import { BooleanComponent } from "./BooleanComponent"; import { ClientSelectComponent } from "./ClientSelectComponent"; @@ -13,6 +14,7 @@ import { RoleComponent } from "./RoleComponent"; import { ScriptComponent } from "./ScriptComponent"; import { StringComponent } from "./StringComponent"; import { TextComponent } from "./TextComponent"; +import { UserProfileAttributeListComponent } from "./UserProfileAttributeListComponent"; export type ComponentProps = Omit & { isDisabled?: boolean; @@ -31,6 +33,7 @@ const ComponentTypes = [ "Group", "MultivaluedList", "ClientList", + "UserProfileAttributeList", "MultivaluedString", "File", "Password", @@ -39,7 +42,7 @@ const ComponentTypes = [ export type Components = (typeof ComponentTypes)[number]; export const COMPONENTS: { - [index in Components]: (props: ComponentProps) => JSX.Element; + [index in Components]: FunctionComponent; } = { String: StringComponent, Text: TextComponent, @@ -50,6 +53,7 @@ export const COMPONENTS: { Map: MapComponent, Group: GroupComponent, ClientList: ClientSelectComponent, + UserProfileAttributeList: UserProfileAttributeListComponent, MultivaluedList: MultiValuedListComponent, MultivaluedString: MultiValuedStringComponent, File: FileComponent, diff --git a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java index 37a11db38e61..4e7b88ad1511 100755 --- a/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java +++ b/server-spi/src/main/java/org/keycloak/provider/ProviderConfigProperty.java @@ -51,6 +51,11 @@ public class ProviderConfigProperty { public static final String MULTIVALUED_LIST_TYPE="MultivaluedList"; public static final String CLIENT_LIST_TYPE="ClientList"; + + /** + * Possibility to select from user attributes defined in the user-profile, but also still have an option to configure custom value + */ + public static final String USER_PROFILE_ATTRIBUTE_LIST_TYPE="UserProfileAttributeList"; public static final String PASSWORD="Password"; /** diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java index 615cb0d32263..313059967d84 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java @@ -83,7 +83,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr property.setName(CONF_USER_ATTRIBUTE); property.setLabel("User Attribute Name"); property.setHelpText("User attribute name to store information into."); - property.setType(ProviderConfigProperty.STRING_TYPE); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); } diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java index 57f65de160ed..af4e00ad8101 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java @@ -68,7 +68,7 @@ public class UserAttributeMapper extends AbstractClaimMapper { property.setName(USER_ATTRIBUTE); property.setLabel("User Attribute Name"); property.setHelpText("User attribute name to store claim. Use email, lastName, and firstName to map to those predefined user properties."); - property.setType(ProviderConfigProperty.STRING_TYPE); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); } diff --git a/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java b/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java index bc827ea55953..c25167eea0c2 100755 --- a/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java @@ -46,7 +46,7 @@ public class HardcodedAttributeMapper extends AbstractIdentityProviderMapper { property.setName(ATTRIBUTE); property.setLabel("User Attribute"); property.setHelpText("Name of user attribute you want to hardcode"); - property.setType(ProviderConfigProperty.STRING_TYPE); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); property = new ProviderConfigProperty(); property.setName(ATTRIBUTE_VALUE); diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java index 3b2d00042783..f3e2251373c5 100755 --- a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java @@ -97,7 +97,7 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper implemen property.setName(USER_ATTRIBUTE); property.setLabel("User Attribute Name"); property.setHelpText("User attribute name to store saml attribute. Use email, lastName, and firstName to map to those predefined user properties."); - property.setType(ProviderConfigProperty.STRING_TYPE); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); } diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/XPathAttributeMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/XPathAttributeMapper.java index 0a52cb14db02..42d5be8c9333 100644 --- a/services/src/main/java/org/keycloak/broker/saml/mappers/XPathAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/saml/mappers/XPathAttributeMapper.java @@ -99,7 +99,7 @@ public class XPathAttributeMapper extends AbstractIdentityProviderMapper impleme property.setName(USER_ATTRIBUTE); property.setLabel("User Attribute Name"); property.setHelpText("User attribute name to store XPath value. Use " + UserModel.EMAIL + ", " + UserModel.FIRST_NAME + ", and " + UserModel.LAST_NAME + " for e-mail, first and last name, respectively."); - property.setType(ProviderConfigProperty.STRING_TYPE); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java index c99f4da142dd..77f77fe43ec9 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java @@ -47,7 +47,7 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O property.setName(ProtocolMapperUtils.USER_ATTRIBUTE); property.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL); property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT); - property.setType(ProviderConfigProperty.STRING_TYPE); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserAttributeMapper.class); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java index 0eb92e2a4b46..9c324f3c1d2b 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java @@ -46,6 +46,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp property.setName(ProtocolMapperUtils.USER_ATTRIBUTE); property.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL); property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); AttributeStatementHelper.setConfigProperties(configProperties); diff --git a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java index e46e09f8d61e..8523f0ac0ee9 100755 --- a/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java +++ b/services/src/main/java/org/keycloak/protocol/saml/mappers/UserPropertyAttributeStatementMapper.java @@ -44,6 +44,7 @@ public class UserPropertyAttributeStatementMapper extends AbstractSAMLProtocolMa property.setName(ProtocolMapperUtils.USER_ATTRIBUTE); property.setLabel(ProtocolMapperUtils.USER_MODEL_PROPERTY_LABEL); property.setHelpText(ProtocolMapperUtils.USER_MODEL_PROPERTY_HELP_TEXT); + property.setType(ProviderConfigProperty.USER_PROFILE_ATTRIBUTE_LIST_TYPE); configProperties.add(property); AttributeStatementHelper.setConfigProperties(configProperties);