Skip to content

Commit

Permalink
Allow selecting attributes from user profile when managing token mapp…
Browse files Browse the repository at this point in the history
…ers (keycloak#26415)

* Allow selecting attributes from user profile when managing token mappers
closes keycloak#24250

Signed-off-by: mposolda <mposolda@gmail.com>

Co-authored-by: Jon Koops <jonkoops@gmail.com>
  • Loading branch information
mposolda and jonkoops authored Jan 25, 2024
1 parent 7797f77 commit 651d99d
Show file tree
Hide file tree
Showing 16 changed files with 88 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ static List<ProviderConfigProperty> 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.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -317,7 +317,7 @@ export default class ProviderPage {
}

createNewMapper(mapperType: string) {
const userModelAttValue = "firstName";
const userModelAttValue = "middleName";
const ldapAttValue = "cn";
const ldapDnValue = "ou=groups";

Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserProfileConfig>();
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 (
<FormGroup
label={t(label!)}
isRequired={required}
labelIcon={<HelpItem helpText={t(helpText!)} fieldLabelId={label!} />}
fieldId={convertedName!}
validated={errors[convertedName!] ? "error" : "default"}
helperTextInvalid={t("required")}
>
<KeySelect
name={convertedName}
rules={required ? { required: true } : {}}
selectItems={convert(config)}
/>
</FormGroup>
);
};
6 changes: 5 additions & 1 deletion js/apps/admin-ui/src/components/dynamic/components.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<ConfigPropertyRepresentation, "type"> & {
isDisabled?: boolean;
Expand All @@ -31,6 +33,7 @@ const ComponentTypes = [
"Group",
"MultivaluedList",
"ClientList",
"UserProfileAttributeList",
"MultivaluedString",
"File",
"Password",
Expand All @@ -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<ComponentProps>;
} = {
String: StringComponent,
Text: TextComponent,
Expand All @@ -50,6 +53,7 @@ export const COMPONENTS: {
Map: MapComponent,
Group: GroupComponent,
ClientList: ClientSelectComponent,
UserProfileAttributeList: UserProfileAttributeListComponent,
MultivaluedList: MultiValuedListComponent,
MultivaluedString: MultiValuedStringComponent,
File: FileComponent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit 651d99d

Please sign in to comment.